Compare commits

..

47 Commits

Author SHA1 Message Date
66792186ca feat: Complete Forum & Admin User Access migration + fix scroll issues
Forum Screens (User Phase 5 - 17 files):
- Migrate all forum list, detail, create, and report screens to OS_Wrapper.
- ViewBeranda, ViewBeranda2, ViewBeranda3: List screens with pull-to-refresh.
- DetailForum, DetailForum2: Comment sections with headers (apply disableFlexGrow fix).
- create, edit, report-*, other-report-*, preview-report-*: Forms with keyboard handling.

Admin Phase 9 (User Access - 2 files):
- index.tsx: List with search and pagination.
- [id]/index.tsx: Detail with status toggle footer.

Scroll Fixes (Critical Bugs):
- Fix "Ghost Scroll" in Android FlatList: Removed TouchableWithoutFeedback and KeyboardAvoidingView wrappers in List Mode.
- Fix Large Header Cut-off: Added optional disableFlexGrow={true} to OS_Wrapper for screens with complex ListHeaderComponents (e.g., Forum Detail).
- Fix Keyboard Dismiss: Changed keyboardShouldPersistTaps to "handled" so taps on empty areas dismiss the keyboard while allowing scroll.

Documentation:
- Update TASK-005 with complete Phase 5 details and new progress totals.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-09 17:48:51 +08:00
c3cf354c28 feat: Migrate Portfolio & Maps screens + perbaiki bug auto-scroll keyboard
Phase 3 - Portfolio Screens (6 files):
- [id]/index.tsx: ViewWrapper → OS_Wrapper (detail dengan pull-to-refresh)
- [id]/edit.tsx: NewWrapper → OS_Wrapper (form + keyboard handling)
- [id]/edit-logo.tsx: ViewWrapper → OS_Wrapper (upload logo)
- [id]/edit-social-media.tsx: ViewWrapper → OS_Wrapper (form + keyboard handling)
- ViewListPortofolio.tsx: NewWrapper → OS_Wrapper (pagination list)
- ScreenPortofolioCreate.tsx: NewWrapper → OS_Wrapper (form + keyboard handling)

Phase 4 - Maps Screens (2 files):
- ScreenMapsCreate.tsx: NewWrapper → OS_Wrapper (form + keyboard handling)
- ScreenMapsEdit.tsx: ViewWrapper → OS_Wrapper (form + keyboard handling)

Bug Fixes:
- Perbaiki auto-scroll keyboard yang membuat input paling atas 'terlempar' keluar layar
- Gunakan UIManager.measure untuk mendapatkan posisi absolut input (pageY) secara akurat
- Logika conditional scroll:
  * Jika input terlihat (di atas keyboard) → TIDAK SCROLL
  * Jika input tertutup keyboard → Scroll secukupnya
- Helper cloneChildrenWithFocusHandler sekarang aktif menyuntikan onFocus handler ke semua TextInput/TextArea/PhoneInput/Select
- Hapus KeyboardAvoidingView dari AndroidWrapper static mode (tidak diperlukan lagi)

Pattern yang diterapkan:
- List screens: contentPaddingBottom=100 (default)
- Form screens: contentPaddingBottom={250} + enableKeyboardHandling
- NO PADDING_INLINE (sesuai preferensi user - mencegah box menyempit)

Dokumentasi:
- Update TASK-005 dengan status lengkap Phase 1-4 (27 files migrated)
- Tambahkan urutan phase baru: Event (Phase 5), Voting (Phase 6), Forum (Phase 7), Donation (Phase 8)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-08 17:27:06 +08:00
6ec839fd67 feat: Migrate Profile, Waiting Room, and Delete Account to OS_Wrapper
Profile Screens (8 files):
- [id]/index.tsx: NewWrapper → OS_Wrapper (list with refresh)
- [id]/edit.tsx: ViewWrapper → OS_Wrapper (form + keyboard handling)
- create.tsx: ViewWrapper → OS_Wrapper (form + keyboard handling)
- [id]/blocked-list.tsx: NewWrapper → OS_Wrapper (pagination list)
- [id]/detail-blocked.tsx: NewWrapper → OS_Wrapper (static with footer)
- [id]/update-background.tsx: ViewWrapper → OS_Wrapper (static with footer)
- [id]/update-photo.tsx: ViewWrapper → OS_Wrapper (static with footer)
- All Profile forms use enableKeyboardHandling + contentPaddingBottom={250}

Other Screens (2 files):
- waiting-room.tsx: NewWrapper → OS_Wrapper (static with refresh + footer)
- delete-account.tsx: ViewWrapper → OS_Wrapper (form + keyboard handling)

Bug Fixes:
- AndroidWrapper: Add refreshControl to ScrollView (fix pull-to-refresh on static mode)

Pattern Applied:
- List screens: contentPaddingBottom=100 (default)
- Form screens: contentPaddingBottom=250 (with TextInput)
- No PADDING_INLINE (user preference - prevents box narrowing)

Documentation:
- Update TASK-005 with Phase 1 completion details

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-08 14:32:11 +08:00
b8f8a361d6 refactor: Adjust OS_Wrapper defaults for better spacing
- Reduce default contentPaddingBottom from 250 to 100
  - Better for list screens (less empty space)
  - Only form screens with TextInput need 250px

- Set OS_ANDROID_PADDING_TOP to 6px
  - More compact tabs on Android

- Update form screens (Create/Edit):
  - Explicit contentPaddingBottom={250}
  - Only screens with TextInput use larger spacing

- Remove unnecessary PADDING_INLINE from detail screens
  - Detail screen doesn't need inline padding

Pattern:
- Default: contentPaddingBottom=100 (list screens)
- Forms: contentPaddingBottom=250 (screens with TextInput)
- contentPadding=0 (per-screen control)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-08 10:31:36 +08:00
1a5ca78041 feat: Complete OS_Wrapper implementation with keyboard handling and PADDING_INLINE
OS_Wrapper System:
- Simplify API: Remove PageWrapper, merge keyboard props into OS_Wrapper
- Add auto-scroll when keyboard appears (Android only)
- Add tap-to-dismiss keyboard for both Static and List modes
- Fix contentPaddingBottom default to 250px (prevent keyboard overlap)
- Change default contentPadding to 0 (per-screen control)
- Remove Platform.OS checks from IOSWrapper and AndroidWrapper

Constants:
- Add PADDING_INLINE constant (16px) for consistent inline padding
- Add OS_PADDING_TOP constants for tab layouts

Job Screens Migration (9 files):
- Apply PADDING_INLINE to all Job screens:
  - ScreenBeranda, ScreenBeranda2
  - ScreenArchive, ScreenArchive2
  - MainViewStatus, MainViewStatus2
  - ScreenJobCreate, ScreenJobEdit
  - Job detail screen

Keyboard Handling:
- Simplified useKeyboardForm hook
- Auto-scroll by keyboard height when keyboard appears
- Track scroll position for accurate scroll targets
- TouchableWithoutFeedback wraps all content for tap-to-dismiss

Documentation:
- Update TASK-005 with Phase 1 completion status
- Update Quick Reference with unified API examples

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 17:50:15 +08:00
502cd7bc65 feat: Implement OS_Wrapper system and migrate all Job screens
Create OS-specific wrapper system:
- Add IOSWrapper (based on NewWrapper for iOS)
- Add AndroidWrapper (based on NewWrapper_V2 with keyboard handling)
- Add OS_Wrapper (auto platform detection)
- Add PageWrapper (with keyboard handling for Android forms)

Migrate all Job screens (8 files):
- ScreenJobCreate: NewWrapper_V2 → PageWrapper
- ScreenJobEdit: NewWrapper_V2 → PageWrapper
- ScreenBeranda2: NewWrapper_V2 → OS_Wrapper
- ScreenArchive2: NewWrapper_V2 → OS_Wrapper
- MainViewStatus2: NewWrapper_V2 → OS_Wrapper
- ScreenBeranda: ViewWrapper → OS_Wrapper
- ScreenArchive: ViewWrapper → OS_Wrapper
- MainViewStatus: ViewWrapper → OS_Wrapper

Benefits:
- Automatic platform detection (no manual Platform.OS checks)
- Consistent tabs behavior across iOS and Android
- Keyboard handling for forms (Android only)
- Cleaner code with unified API

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-07 14:14:00 +08:00
44d9025afe refactor: Update header components and improve job edit footer layout
- Replace Waiting Room header with AppHeader component
- Replace Profile Create header with AppHeader with showBack=false
- Wrap Job Edit submit button with BoxButtonOnFooter for consistent footer layout

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-06 17:49:28 +08:00
b34bc3799e feat: Add default contentPadding to NewWrapper_V2
- Set default contentPadding to 16px for consistent spacing
- Added contentPadding prop to BaseProps interface
- Updated documentation in TASK-004

Benefits:
- All screens automatically have 16px padding
- Cleaner code (no need to specify padding everywhere)
- Still customizable (set to 0 or custom value if needed)
- Box content not too tight to screen edges

Phase 1 completed with consistent padding!

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-02 17:16:19 +08:00
7cb4f30ae9 refactor: Replace NewWrapper with NewWrapper_V2 for all Job screens
- ScreenBeranda2.tsx: NewWrapper → NewWrapper_V2
- ScreenArchive2.tsx: NewWrapper → NewWrapper_V2
- MainViewStatus2.tsx: NewWrapper → NewWrapper_V2

All Job screens now using NewWrapper_V2 with:
- Better keyboard handling
- Footer width 100%
- Content padding bottom 80px
- Auto-scroll to focused input (for forms)
- No white area when keyboard close

Phase 1 completed: All Job screens migrated!

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-02 16:13:23 +08:00
0f552443c4 refactor: Cleanup test files and migrate Job Detail screen
- Delete ScreenJobCreate2.tsx and ScreenJobEdit2.tsx (test files)
- Delete TestWrapper.tsx and TestKeyboardInput.tsx (test components)
- Delete test pages (test-keyboard.tsx, test-keyboard-bug.tsx)
- Update create.tsx to use ScreenJobCreate (not test version)
- Update edit.tsx to use ScreenJobEdit (not test version)
- Migrate Job Detail screen to NewWrapper_V2
- Remove TestWrapper from exports
- Clean up imports

Phase 1 cleanup completed!

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-02 15:31:26 +08:00
90bc8ae343 feat: Migrate Job screens to NewWrapper_V2 with keyboard handling
- Migrate ScreenJobCreate.tsx to NewWrapper_V2
    - Migrate ScreenJobEdit.tsx to NewWrapper_V2
    - Add NewWrapper_V2 component with auto-scroll keyboard handling
    - Add useKeyboardForm hook for keyboard management
    - Add FormWrapper component for forms
    - Create ScreenJobEdit.tsx from edit route (separation of concerns)
    - Add documentation for keyboard implementation
    - Add TASK-004 migration plan
    - Fix: Footer width 100% with safe positioning
    - Fix: Content padding bottom 80px for navigation bar
    - Fix: Auto-scroll to focused input
    - Fix: No white area when keyboard close
    - Fix: Footer not raised after keyboard close

    Phase 1 completed: Job screens migrated

### No Issue
2026-04-02 15:07:10 +08:00
98f8c7e2bf Fix layout tabs pada komponen
Fix home tabs

### No Issue
2026-04-01 17:17:12 +08:00
81bbd8e6b0 Clean project and release with new domain: hipmi.muku.id
### No issue
2026-03-31 17:07:27 +08:00
57159d2c45 update app config domain
### No Issue
2026-03-31 14:29:19 +08:00
66373fa65b Clean code
Update android domain

### No Issue
2026-03-31 14:28:43 +08:00
6d545f2af9 Fix change yang tertinggal 2026-03-30 17:39:53 +08:00
eeb95336f2 Fix version & tabs android
### No Issue
2026-03-30 17:38:31 +08:00
6fb3b229c3 Fix andorid 2026-03-27 17:59:13 +08:00
76deec9c53 Fix tabs agar bisa di klik
### Issue: Di android batas atas kurang turun
2026-03-26 17:37:31 +08:00
31948f71db Fix: NewWrapper footer position dengan SafeAreaView
Problem:
- Footer component tertutup atau scroll melebihi layar
- Tabs tidak bisa diklik karena footer floating
- Height OS_HEIGHT tidak konsisten di berbagai device

Solution:
 Footer menggunakan position: absolute untuk stay di bawah
 Konten ScrollView/FlatList mendapat paddingBottom: OS_HEIGHT
 SafeAreaView hanya untuk area aman, bukan height control
 Footer tetap visible di semua ukuran layar

Changes:
- Add styles.footerContainer dengan position: absolute
- Add paddingBottom ke contentContainerStyle (ScrollView & FlatList)
- Remove height: OS_HEIGHT dari SafeAreaView
- Footer stay di bottom dengan proper safe area handling

Result:
- Footer selalu terlihat di bawah layar
- Konten tidak tertutup footer (ada padding)
- Tabs bisa diklik dengan baik
- Konsisten di iOS & Android
- Respect safe area insets

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 12:28:07 +08:00
16decd89c8 Refactor: apply PhoneInputCustom to PortofolioEdit
Changes:
- Replace react-native-international-phone-number with PhoneInputCustom
- Remove ICountry dependency, use CountryData from constants
- Add phone number state management
- Implement country detection from existing phone number
- Auto-detect country based on calling code on load
- Improve phone number formatting logic

Features Applied:
 NO emoji flags - only calling codes (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 Auto-detect country from saved phone number
 Real-time phone number formatting
 Auto-update country code on change
 Consistent with LoginView & ScreenPortofolioCreate

Phone Detection Logic:
- Load existing phone number from API
- Detect country by matching calling code
- Extract phone number without country code for display
- Set detected country for country picker
- Re-format on country change

UI:
- Phone Input: [+62 ⌄ | xxx-xxx-xxx]
- Country Picker: Modal with search
- Display: Country name + calling code only

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 12:17:05 +08:00
ecbcc12abf Refactor: apply PhoneInputCustom to ScreenPortofolioCreate
Changes:
- Replace react-native-international-phone-number with PhoneInputCustom
- Remove ICountry dependency, use CountryData from constants
- Update state management (inputValue → phoneNumber)
- Improve phone number formatting logic
- Add handleCountryChange for better country switching

Features Applied:
 NO emoji flags - only calling codes (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 Real-time phone number formatting
 Auto-update country code on change
 Consistent with LoginView implementation

Phone Input Logic:
- Format on every phone change
- Re-format when country changes
- Remove duplicate country codes
- Remove leading zeros
- Store E.164 format in API data

UI:
- Phone Input: [+62 ⌄ | xxx-xxx-xxx]
- Country Picker: Modal with search
- Display: Country name + calling code only

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 11:42:54 +08:00
0cb734e790 Refactor: apply PhoneInputCustom to LoginView
Changes:
- Replace react-native-international-phone-number with PhoneInputCustom
- Remove dependency on ICountry, use CountryData from constants
- Add KeyboardAvoidingView for better iOS keyboard handling
- Improve validation with libphonenumber-js
- Add proper phone number formatting
- Update state management (inputValue → phoneNumber)

Features Applied:
 NO emoji flags - only calling codes (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 Better keyboard handling on iOS
 Real-time validation with libphonenumber-js
 Auto-formatting for international numbers
 Reusable component

UI:
- Phone Input: [+62 ⌄ | 812-3456-7890]
- Country Picker: Modal with search
- Display: Country name + calling code only

Validation:
- Check phone number length
- Validate with libphonenumber-js
- Format to E.164 on login
- Error handling with Toast

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 11:33:18 +08:00
0d2fef1878 Feat: add reusable PhoneInput component without flags
New Components:
- PhoneInputCustom: Reusable phone input without emoji flags
- constants/countries.ts: Country data with calling codes only

Features:
 NO emoji flags - only country name + calling code (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 15 countries supported
 Helper functions: getCountryByCallingCode, getCountryByCode, searchCountries
 Fully typed with TypeScript
 Reusable across the app
 Maximum compatibility (no emoji rendering issues)

UI Design:
- Phone Input: [+62 ⌄ | 812-3456-7890]
- Country Picker: Modal with search
- Display: Country name + calling code only

Usage:
import { PhoneInputCustom } from '@/components';
import { DEFAULT_COUNTRY } from '@/constants/countries';

<PhoneInputCustom
  value={phoneNumber}
  onChangePhoneNumber={setPhoneNumber}
  selectedCountry={selectedCountry}
  onChangeCountry={setSelectedCountry}
/>

Benefits:
 Works on ALL iOS versions (no emoji issues)
 Consistent across all platforms
 Faster render (no emoji/image loading)
 Cleaner code structure
 Easy to maintain

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 11:27:59 +08:00
2e58f8c7b4 Docs: add task breakdown untuk fix phone input iOS 16+
- Add TASKS/fix-phone-input-ios-16.md dengan analisis lengkap
- 3 opsi solusi: KeyboardAvoidingView, Keyboard Controller, Custom Input
- Detailed implementation guidelines
- Testing checklist untuk iOS 16, 17, 18

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-25 16:39:48 +08:00
b6cd308b0b Refactor pagination implementation dan perbaikan UI
- Add default page parameter di apiAllUser
- Refactor MainView_V2.tsx dengan separate render functions
- Update pagination pageSize menjadi 10 di Forum
- Fix iOS height constant dan tab styling
- Rename Admin_ScreenPortofolioCreate ke ScreenPortofolioCreate
- Add TASKS documentation folder

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-25 16:22:54 +08:00
f68deab8c0 Fix App Header
Layouts & Navigation
- app/(application)/_layout.tsx
- app/(application)/(user)/_layout.tsx
- app/(application)/(user)/event/(tabs)/_layout.tsx
- app/(application)/(user)/job/(tabs)/_layout.tsx
- app/(application)/(user)/voting/(tabs)/_layout.tsx
- app/(application)/(user)/portofolio/_layout.tsx
- app/(application)/(user)/profile/_layout.tsx
- app/(application)/admin/_layout.tsx
- app/+not-found.tsx

User – File
- app/(application)/(file)/[id].tsx

User – Collaboration
- app/(application)/(user)/collaboration/[id]/index.tsx
- app/(application)/(user)/collaboration/[id]/detail-project-main.tsx
- app/(application)/(user)/collaboration/[id]/detail-participant.tsx
- app/(application)/(user)/collaboration/[id]/[detail]/info.tsx
- app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx

User – Donation
- app/(application)/(user)/donation/[id]/index.tsx
- app/(application)/(user)/donation/[id]/[status]/detail.tsx
- app/(application)/(user)/donation/[id]/(news)/[news]/index.tsx

User – Event
- app/(application)/(user)/event/[id]/[status]/detail-event.tsx
- app/(application)/(user)/event/[id]/confirmation.tsx
- app/(application)/(user)/event/[id]/contribution.tsx
- app/(application)/(user)/event/[id]/history.tsx
- app/(application)/(user)/event/[id]/publish.tsx

User – Investment
- app/(application)/(user)/investment/[id]/index.tsx
- app/(application)/(user)/investment/[id]/[status]/detail.tsx
- app/(application)/(user)/investment/[id]/(my-holding)/[id].tsx
- app/(application)/(user)/investment/[id]/(news)/[news]/index.tsx

User – Job
- app/(application)/(user)/job/[id]/[status]/detail.tsx

User – Portofolio
- app/(application)/(user)/portofolio/[id]/index.tsx

User – Profile
- app/(application)/(user)/profile/[id]/index.tsx

User – Voting
- app/(application)/(user)/voting/[id]/index.tsx
- app/(application)/(user)/voting/[id]/[status]/detail.tsx
- app/(application)/(user)/voting/[id]/contribution.tsx
- app/(application)/(user)/voting/[id]/history.tsx

Components
- components/Button/BackButtonFromNotification.tsx
- components/_ShareComponent/AppHeader.tsx

Admin Screens
- screens/Admin/Notification-Admin/ScreenNotificationAdmin.tsx
- screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx

Screens – Donation
- screens/Donation/ScreenListOfNews.tsx
- screens/Donation/ScreenRecapOfNews.tsx

Screens – Forum
- screens/Forum/ViewBeranda.tsx
- screens/Forum/ViewBeranda2.tsx
- screens/Forum/ViewBeranda3.tsx

Screens – Investment
- screens/Invesment/Document/ScreenRecapOfDocument.tsx
- screens/Invesment/News/ScreenListOfNews.tsx
- screens/Invesment/News/ScreenRecapOfNews.tsx

Screens – Notification
- screens/Notification/ScreenNotification_V1.tsx
- screens/Notification/ScreenNotification_V2.tsx

iOS
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

Docs
- QWEN.md

### Issue: Tabs can't clicked
2026-03-13 16:41:34 +08:00
37d2fbe48a Fix code/
User Pages
- app/(application)/(user)/home.tsx
- app/(application)/(user)/portofolio/[id]/create.tsx

Components
- components/_ShareComponent/AppHeader.tsx

Screens
- screens/Portofolio/ScreenPortofolioCreate.tsx

Config & Dependencies
- app.config.js
- package.json
- bun.lock

iOS
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

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

### No issue
2026-03-12 16:50:02 +08:00
57c9215771 Feat validasi mobile otp
modified:   context/AuthContext.tsx
        modified:   screens/Authentication/VerificationView.tsx
        modified:   service/api-config.ts

### No Issue
2026-03-11 15:04:32 +08:00
4efdbd3c7b feat: update admin features, user confirmation, and native configs
- Admin: Update layout, notification bell, and event detail screen
- User: Improve event confirmation flow
- Config: Update AndroidManifest, Info.plist, entitlements, and app.config.js

### No Issue
2026-03-11 11:29:20 +08:00
ad32eb6fe6 feat: implement deep linking & universal links for event confirmation
- Add QR code toggle for HTTPS/Custom Scheme links
- Add Universal Links (iOS) and App Links (Android) support
- Create deep link route handler with platform detection
- Add .well-known files for domain verification
- Fix Content-Type headers for apple-app-site-association
- Add environment configuration for staging & production
- Add comprehensive testing documentation

Testing:
- QR scan → Safari → App switch working 
- Platform detection working 
- Auto redirect to custom scheme working 
- Web fallback JSON response working 

### No Issue
2026-03-09 16:39:01 +08:00
a5026cc285 add: Admin Event detail screen dan komponen pendukung
Deskripsi:

Menambahkan halaman detail event pada admin panel dengan status parameter

Menambahkan beberapa komponen UI untuk menampilkan detail event, drawer informasi, dan QR Code

Update konfigurasi aplikasi dan iOS project

Perbaikan pada halaman verifikasi authentication

Update dokumentasi prompt untuk Qwen

File yang diubah:

Modified

app.config.js

app/(application)/admin/event/[id]/[status]/index.tsx

docs/prompt-for-qwen-code.md

ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

ios/HIPMIBadungConnect/Info.plist

screens/Authentication/VerificationView.tsx

New Admin Event Components

screens/Admin/Event/BoxEventDetail.tsx

screens/Admin/Event/EventDetailDrawer.tsx

screens/Admin/Event/EventDetailQRCode.tsx

screens/Admin/Event/ScreenEventDetail.tsx

### No Issue
2026-03-06 16:39:55 +08:00
836ef709d2 Fix bug home
### No Issue
2026-03-05 16:41:29 +08:00
3bbee15c3a Perbaikan Bug & Error Handling: │
│                                                                                                            │
  │    1. Device Token Registration Error (HTTP 500)                                                           │
  │       - File: service/api-device-token.ts                                                                  │
  │       - Fix: Hapus nested data wrapper pada payload                                                        │
  │       - Improvement: Tambahkan error logging detail                                                        │
  │                                                                                                            │
  │    2. Uncaught Promise Errors                                                                              │
  │       - File: components/Notification/NotificationInitializer.tsx                                          │
  │       - Fix: Better error handling untuk device token registration                                         │
  │       - File: app/(application)/(user)/home.tsx                                                            │
  │       - Fix: Add .catch() untuk userData() dan error handling apiUser()                                    │
  │       - File: app/(application)/(user)/profile/[id]/index.tsx                                              │
  │       - Fix: Add error handling untuk apiProfile(), apiUser(), userData()                                  │
  │                                                                                                            │
  │    3. UI Improvements                                                                                      │
  │       - File: app/(application)/(user)/home.tsx                                                            │
  │       - Feature: 4 skeleton lingkaran untuk loading state grid features                                    │
  │                                                                                                            │
  │    4. Maps Migration                                                                                       │
  │       - File: app/(application)/admin/maps.tsx                                                             │
  │       - Change: Replace react-native-maps dengan MapsV2Custom (Maplibre)                                   │
  │       - Cleanup: Hapus unused imports dan interfaces                                                       │
  │                                                                                                            │
  │   Files Modified (7)                                                                                       │
  │    - app/(application)/(user)/home.tsx                                                                     │
  │    - app/(application)/(user)/profile/[id]/index.tsx                                                       │
  │    - app/(application)/admin/maps.tsx                                                                      │
  │    - components/Notification/NotificationInitializer.tsx                                                   │
  │    - service/api-device-token.ts                                                                           │
  │    - constants/constans-value.ts                                                                           │
  │    - screens/Home/bottomFeatureSection.tsx                                                                 │
  │    - screens/UserSeach/MainView_V2.tsx

### No Issue
2026-03-04 16:39:57 +08:00
ad7dbaf162 Fix Bug DB
User Pages
- app/(application)/(user)/home.tsx
- app/(application)/(user)/portofolio/[id]/index.tsx
- app/(application)/(user)/profile/[id]/index.tsx

Home
- screens/Home/bottomFeatureSection.tsx

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

Service
- service/api-device-token.ts

Config & iOS
- app.config.js
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2026-03-03 16:44:45 +08:00
9c94ec0454 Fix Bug
Maps Platform Update
- components/Map/MapSelectedPlatform.tsx
- components/Map/MapSelectedV2.tsx

Maps Screens
- screens/Maps/ScreenMapsCreate.tsx
- screens/Maps/ScreenMapsEdit.tsx

Home
- screens/Home/bottomFeatureSection.tsx

Config & Native
- app.config.js
- android/app/build.gradle
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2026-03-02 16:34:24 +08:00
4c63485a5b Clean Code Edit Maps
Maps Edit Feature
- app/(application)/(user)/maps/[id]/edit.tsx
- components/Map/MapSelectedV2.tsx

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

New Screen
- screens/Maps/ScreenMapsEdit.tsx

### No Issue
2026-03-02 10:31:29 +08:00
f5d09a2906 Fix create maps
iOS Project
- HIPMIBadungConnect.xcodeproj/project.pbxproj

Maps & Location Screens
- screens/Maps/MapsView2.tsx
- screens/Portofolio/BusinessLocationSection.tsx

New Map Components
- components/Map/MapsV2Custom.tsx
- components/Map/SelectLocationMap.tsx

### No Issue
2026-02-27 16:57:01 +08:00
67070bb2f1 Fix Maps
iOS Project
- HIPMIBadungConnect.xcodeproj/project.pbxproj

Maps & Location Screens
- screens/Maps/MapsView2.tsx
- screens/Portofolio/BusinessLocationSection.tsx

New Map Components
- components/Map/MapsV2Custom.tsx
- components/Map/SelectLocationMap.tsx

### No Issue
2026-02-26 18:04:45 +08:00
fb19ec60b2 Fix Maps
iOS Project
- HIPMIBadungConnect.xcodeproj/project.pbxproj

Maps & Location Screens
- screens/Maps/MapsView2.tsx
- screens/Portofolio/BusinessLocationSection.tsx

New Map Components
- components/Map/MapsV2Custom.tsx
- components/Map/SelectLocationMap.tsx

### No Issue
2026-02-26 17:53:23 +08:00
e8f5c5b174 User Maps
- app/(application)/(user)/maps/index.tsx
- screens/Maps/MapsView2.tsx

New Maps Component
- screens/Maps/DrawerMaps.tsx

Docs / Backup
- docs/PODS.back

### No Issue
2026-02-25 16:46:37 +08:00
74a4d88277 Fix POD File & Maps
User Maps
- app/(application)/(user)/maps/index.tsx
- screens/Maps/MapsView2.tsx

iOS
- HIPMIBadungConnect.xcodeproj/project.pbxproj
- Podfile.lock
- HIPMIBadungConnect.xcworkspace/xcshareddata/

New Maps Component
- screens/Maps/DrawerMaps.tsx

Docs / Backup
- docs/PODS.back

### No Issue
2026-02-25 16:37:42 +08:00
2ad93a26a8 Tampilan maps android
- screens/Maps/MapsView2.tsx

### No Issue
2026-02-24 17:57:14 +08:00
768b0caa9e Fix maps
### No Issue
2026-02-24 16:48:37 +08:00
208b0ce813 Fix Maps
Config & Dependencies
- app.config.js
- package.json
- bun.lock
- ios/Podfile
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

User Pages
- app/(application)/(user)/maps/index.tsx
- app/(application)/(user)/portofolio/[id]/index.tsx

Maps
- screens/Maps/MapsView2.tsx

### No Issue
2026-02-24 16:41:43 +08:00
66e6aebf41 Fixed Admin Package
Config & Dependencies
- app.config.js
- package.json
- bun.lock
- ios/Podfile
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

Home
- screens/Home/tabsList.ts

Maps
- screens/Maps/MapsView2.tsx

### No Issue
2026-02-24 07:18:01 +08:00
32a42d1b60 Fix UI Admin
User & Image
- app/(application)/(image)/take-picture/[id]/index.tsx
- app/(application)/(user)/home.tsx

Admin – Forum
- app/(application)/admin/forum/[id]/index.tsx
- app/(application)/admin/forum/[id]/list-comment.tsx
- app/(application)/admin/forum/[id]/list-report-comment.tsx

Admin Screens – Forum
- screens/Admin/Forum/ScreenForumDetailReportPosting.tsx
- screens/Admin/Forum/ScreenForumReportComment.tsx
- screens/Admin/Forum/ScreenForumReportPosting.tsx

New Admin Screens – Forum
- screens/Admin/Forum/ScreenForumDetailReportComment.tsx
- screens/Admin/Forum/ScreenForumListComment.tsx

Home
- screens/Home/bottomFeatureSection.tsx

Service
- service/api-admin/api-admin-forum.ts

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

### No Issue
2026-02-20 16:48:26 +08:00
180 changed files with 10453 additions and 4181 deletions

3
.gitignore vendored
View File

@@ -81,4 +81,7 @@ yarn-error.*
# typescript
*.tsbuildinfo
# secrets
secrets/
# @end expo-cli

8
.qwen/settings.json Normal file
View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git add *)"
]
},
"$version": 3
}

7
.qwen/settings.json.orig Normal file
View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(git add *)"
]
}
}

62
QWEN.md
View File

@@ -10,7 +10,7 @@ HIPMI Mobile is a cross-platform mobile application built with Expo and React Na
- **Architecture**: File-based routing with Expo Router
- **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
@@ -381,13 +381,13 @@ apiConfig.interceptors.request.use(async (config) => {
- Push Notifications (FCM)
- Configured for both iOS and Android
### Mapbox
- Map integration via `@rnmapbox/maps`
### Maplibre
- Map integration via `@maplibre/maplibre-react-native`
- Location permissions configured
### Deep Linking
- Scheme: `hipmimobile://`
- HTTPS: `cld-dkr-staging-hipmi.wibudev.com`
- HTTPS: `cld-dkr-hipmi-stg.wibudev.com`
- Configured for both platforms
### Camera
@@ -475,10 +475,34 @@ 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/COMMIT_NOTES.md` - Commit notes and guidelines
- `docs/hipmi-note.md` - Build and deployment notes
- `docs/prompt-for-qwen-code.md` - Development prompts and patterns
@@ -488,3 +512,31 @@ bun install
- [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)
## Qwen Added Memories
- OS_Wrapper contentPaddingBottom pattern:
- Default: contentPaddingBottom=100 (untuk list screens)
- Forms: contentPaddingBottom=250 (HANYA untuk screens yang punya TextInput/TextArea)
- contentPadding=0 (default, per-screen control)
- OS_ANDROID_PADDING_TOP=6 (compact tabs)
- OS_IOS_PADDING_TOP=12
- PADDING_INLINE=16 (constant)
Contoh:
```tsx
// List screen (default 100px)
<OS_Wrapper listData={data} renderItem={renderItem} />
// Form screen (explicit 250px)
<OS_Wrapper enableKeyboardHandling contentPaddingBottom={250}>
<FormWithTextInput />
</OS_Wrapper>
```
- PADDING_INLINE usage pattern - User preference:
- PADDING_INLINE (16px) TIDAK selalu diperlukan
- User remove PADDING_INLINE dari Profile screens karena mempersempit box tampilan
- Decision: Tambahkan PADDING_INLINE HANYA jika diperlukan per-screen, jangan default
- User akan review dan tambahkan sendiri jika perlu
Profile screens: PADDING_INLINE dihapus dari edit.tsx dan create.tsx

View File

@@ -100,8 +100,8 @@ packagingOptions {
applicationId 'com.bip.hipmimobileapp'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 4
versionName "1.0.1"
versionCode 5
versionName "1.0.2"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -37,7 +37,7 @@
</intent-filter>
<intent-filter android:autoVerify="true" data-generated="true">
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https" android:host="cld-dkr-staging-hipmi.wibudev.com" android:pathPrefix="/"/>
<data android:scheme="https" android:host="hipmi.muku.id" android:pathPrefix="/"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>

View File

@@ -6,32 +6,17 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.google.gms:google-services:4.4.1'
classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
}
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}
apply plugin: "expo-root-project"
apply plugin: "com.facebook.react.rootproject"
// @generated begin @rnmapbox/maps-v2-maven - expo prebuild (DO NOT MODIFY) sync-d4ccbfdff48fdba3138b02a8ba41b9722af001d8
allprojects {
repositories {
maven {
url 'https://api.mapbox.com/downloads/v2/releases/maven'
// Authentication is no longer required as per Mapbox's removal of download token requirement
// See: https://github.com/mapbox/mapbox-maps-flutter/issues/775
// Keeping this as optional for backward compatibility
def token = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: System.getenv('RNMAPBOX_MAPS_DOWNLOAD_TOKEN')
if (token) {
authentication { basic(BasicAuthentication) }
@@ -41,7 +26,11 @@ allprojects {
}
}
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}
// @generated end @rnmapbox/maps-v2-maven
apply plugin: "expo-root-project"
apply plugin: "com.facebook.react.rootproject"

View File

@@ -1,10 +1,21 @@
// app.config.js
require("dotenv").config();
// const isDev = process.env.NODE_ENV === "development";
// const isStaging = process.env.NEXT_PUBLIC_ENV === "staging";
// const isProd = process.env.NEXT_PUBLIC_ENV === "production";
// Domain berdasarkan environment
// const domain = isDev
// ? "localhost:3000"
// : isStaging
// ? "cld-dkr-hipmi-stg.wibudev.com"
// : "hipmi.muku.id"; // Production domain
export default {
name: "HIPMI Badung Connect",
slug: "hipmi-mobile",
version: "1.0.1",
version: "1.0.2",
orientation: "portrait",
icon: "./assets/images/icon.png",
scheme: "hipmimobile",
@@ -14,25 +25,27 @@ export default {
ios: {
supportsTablet: true,
bundleIdentifier: "com.anonymous.hipmi-mobile",
googleServicesFile: "./ios/HIPMIBadungConnect/GoogleService-Info.plist",
googleServicesFile: "./secrets/GoogleService-Info.plist",
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
NSLocationWhenInUseUsageDescription:
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
},
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "21",
associatedDomains: [
"applinks:hipmi.muku.id",
],
buildNumber: "7",
},
android: {
googleServicesFile: "./google-services.json",
googleServicesFile: "./secrets/google-services.json",
adaptiveIcon: {
foregroundImage: "./assets/images/splash-icon.png",
backgroundColor: "#ffffff",
},
edgeToEdgeEnabled: true,
package: "com.bip.hipmimobileapp",
versionCode: 4,
versionCode: 5,
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
intentFilters: [
{
@@ -41,7 +54,7 @@ export default {
data: [
{
scheme: "https",
host: "cld-dkr-staging-hipmi.wibudev.com",
host: "hipmi.muku.id",
pathPrefix: "/",
},
],
@@ -57,6 +70,7 @@ export default {
},
plugins: [
"./plugins/withCustomConfig",
"expo-router",
"expo-web-browser",
[
@@ -77,7 +91,6 @@ export default {
},
],
"expo-font",
"@rnmapbox/maps",
"@react-native-firebase/app",
[
"expo-notifications",
@@ -87,6 +100,7 @@ export default {
iosDisplayInForeground: true,
},
],
"@maplibre/maplibre-react-native",
],
experiments: {

View File

@@ -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"]}>

View File

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

View File

@@ -1,29 +1,27 @@
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
name="waiting-room"
options={{
title: "Waiting Room",
headerBackVisible: false,
header: () => <AppHeader title="Waiting Room" />,
}}
/>
@@ -47,8 +45,7 @@ export default function UserLayout() {
<Stack.Screen
name="user-search/index"
options={{
title: "Pencarian Pengguna",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Pencarian Pengguna" />,
}}
/>
@@ -71,10 +68,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 +90,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 +120,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 +145,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 +167,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 +195,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 +205,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 +302,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 +322,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 +344,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 +456,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 +474,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 +528,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 +596,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 +622,7 @@ export default function UserLayout() {
<Stack.Screen
name="marketplace/index"
options={{
title: "Market Place",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Market Place" />,
}}
/>
</Stack>

View File

@@ -2,35 +2,64 @@ import { IconHome } from "@/components/_Icon";
import { TabsStyles } from "@/styles/tabs-styles";
import { Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
function CollaborationTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
export default function CollaborationTabsLayout() {
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
}}
/>
<Tabs.Screen
name="participant"
options={{
title: "Partisipan",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="people" color={color} />
),
}}
/>
<Tabs.Screen
name="group"
options={{
title: "Grup",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="chatbox-ellipses" color={color} />
),
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="participant"
options={{
title: "Partisipan",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="people" color={color} />
),
}}
/>
<Tabs.Screen
name="group"
options={{
title: "Grup",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="chatbox-ellipses" color={color} />
),
}}
/>
</Tabs>
</View>
);
}
export default function CollaborationTabsLayout() {
return <CollaborationTabsWrapper />;
}

View File

@@ -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 />} />,
}}
/>

View File

@@ -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`)}
/>
}
/>
),
}}

View File

@@ -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)} />
}
/>
),
}}
/>

View File

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

View File

@@ -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)} />
}
/>
),
}}
/>

View File

@@ -3,10 +3,10 @@ import {
BaseBox,
ButtonCustom,
CenterCustom,
OS_Wrapper,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiDeleteUser } from "@/service/api-client/api-user";
@@ -68,7 +68,10 @@ export default function DeleteAccount() {
return (
<>
<ViewWrapper>
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
>
<StackCustom>
<BaseBox>
<StackCustom>
@@ -105,7 +108,7 @@ export default function DeleteAccount() {
</StackCustom>
</BaseBox>
</StackCustom>
</ViewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -5,33 +5,62 @@ import {
FontAwesome5
} from "@expo/vector-icons";
import { Tabs } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
function DonationTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
export default function InvestmentTabsLayout() {
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Galang Dana",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="my-donation"
options={{
title: "Donasi Saya",
tabBarIcon: ({ color }) => (
<FontAwesome5 name="donate" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Galang Dana",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="my-donation"
options={{
title: "Donasi Saya",
tabBarIcon: ({ color }) => (
<FontAwesome5 name="donate" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
</Tabs>
</View>
);
}
export default function DonationTabsLayout() {
return <DonationTabsWrapper />;
}

View File

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

View File

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

View File

@@ -185,7 +185,6 @@ export default function DonationEdit() {
return (
<NewWrapper
hideFooter
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom

View File

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

View File

@@ -114,7 +114,6 @@ export default function DonationCreateStory() {
return (
<NewWrapper
hideFooter
footerComponent={
<>
<BoxButtonOnFooter>

View File

@@ -127,7 +127,6 @@ export default function DonationCreate() {
return (
<NewWrapper
hideFooter
footerComponent={
<>
<BoxButtonOnFooter>

View File

@@ -4,64 +4,87 @@ 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";
export default function EventTabsLayout() {
const navigation = useNavigation();
import { router, Tabs, useLocalSearchParams } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
import { OS_ANDROID_HEIGHT, OS_IOS_HEIGHT } from "@/constants/constans-value";
function EventTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
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.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: OS_IOS_HEIGHT,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: OS_ANDROID_HEIGHT + paddingBottom,
},
}),
header: () => (
<AppHeader
title="Event"
left={
<BackButtonFromNotification
from={from || ""}
category={category}
/>
}
/>
),
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="contribution"
options={{
title: "Kontribusi",
tabBarIcon: ({ color }) => <IconContribution color={color} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Riwayat",
tabBarIcon: ({ color }) => <IconHistory color={color} />,
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="contribution"
options={{
title: "Kontribusi",
tabBarIcon: ({ color }) => <IconContribution color={color} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Riwayat",
tabBarIcon: ({ color }) => <IconHistory color={color} />,
}}
/>
</Tabs>
</View>
);
}
export default function EventTabsLayout() {
return <EventTabsWrapper />;
}

View File

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

View File

@@ -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 = ({

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,8 @@ import {
BoxButtonOnFooter,
ButtonCustom,
LoaderCustom,
OS_Wrapper,
TextAreaCustom,
ViewWrapper,
} from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning";
import { apiForumGetOne, apiForumUpdate } from "@/service/api-client/api-forum";
@@ -88,7 +88,11 @@ export default function ForumEdit() {
};
return (
<ViewWrapper footerComponent={buttonFooter()}>
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
footerComponent={buttonFooter()}
>
{!loadingGetData ? (
<TextAreaCustom
placeholder="Ketik diskusi anda..."
@@ -102,6 +106,6 @@ export default function ForumEdit() {
) : (
<LoaderCustom />
)}
</ViewWrapper>
</OS_Wrapper>
);
}

View File

@@ -1,8 +1,8 @@
import {
BoxButtonOnFooter,
ButtonCustom,
OS_Wrapper,
TextAreaCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
@@ -62,13 +62,17 @@ export default function ForumOtherReportCommentar() {
return (
<>
<ViewWrapper footerComponent={handleSubmit}>
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
footerComponent={handleSubmit}
>
<TextAreaCustom
placeholder="Laporkan Komentar"
value={value}
onChangeText={setValue}
/>
</ViewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -1,8 +1,8 @@
import {
BoxButtonOnFooter,
ButtonCustom,
OS_Wrapper,
TextAreaCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
@@ -61,13 +61,17 @@ export default function ForumOtherReportPosting() {
);
return (
<>
<ViewWrapper footerComponent={handleSubmit}>
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
footerComponent={handleSubmit}
>
<TextAreaCustom
placeholder="Laporkan Diskusi"
value={value}
onChangeText={setValue}
/>
</ViewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -1,6 +1,6 @@
import {
BaseBox,
NewWrapper,
OS_Wrapper,
Spacing,
StackCustom,
TextCustom,
@@ -41,7 +41,7 @@ export default function ForumPreviewReportComment() {
return (
<>
<NewWrapper>
<OS_Wrapper>
<StackCustom>
<TextCustom color="red" bold>
Komentar anda telah melanggar aturan forum ! Admin mengambil
@@ -85,7 +85,7 @@ export default function ForumPreviewReportComment() {
</BaseBox>
))
)}
</NewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -1,6 +1,6 @@
import {
BaseBox,
NewWrapper,
OS_Wrapper,
Spacing,
StackCustom,
TextCustom,
@@ -41,7 +41,7 @@ export default function ForumPreviewReportPosting() {
return (
<>
<NewWrapper>
<OS_Wrapper>
<StackCustom>
<TextCustom color="red" bold>
Postingan anda telah melanggar aturan forum ! Admin mengambil
@@ -85,7 +85,7 @@ export default function ForumPreviewReportPosting() {
</BaseBox>
))
)}
</NewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -1,9 +1,9 @@
import {
ButtonCustom,
LoaderCustom,
OS_Wrapper,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
@@ -69,7 +69,7 @@ export default function ForumReportCommentar() {
return (
<>
<ViewWrapper>
<OS_Wrapper>
{isLoadingList ? (
<LoaderCustom />
) : (
@@ -101,7 +101,7 @@ export default function ForumReportCommentar() {
<Spacing />
</StackCustom>
)}
</ViewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -2,9 +2,9 @@ import {
AlertDefaultSystem,
ButtonCustom,
LoaderCustom,
OS_Wrapper,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
@@ -73,7 +73,7 @@ export default function ForumReportPosting() {
return (
<>
<ViewWrapper>
<OS_Wrapper>
{isLoadingList ? (
<LoaderCustom />
) : (
@@ -114,7 +114,7 @@ export default function ForumReportPosting() {
<Spacing />
</StackCustom>
)}
</ViewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -1,8 +1,8 @@
import {
BoxButtonOnFooter,
ButtonCustom,
OS_Wrapper,
TextAreaCustom,
ViewWrapper,
} from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning";
import { useAuth } from "@/hooks/use-auth";
@@ -67,7 +67,11 @@ export default function ForumCreate() {
);
return (
<ViewWrapper footerComponent={buttonFooter}>
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
footerComponent={buttonFooter}
>
<TextAreaCustom
placeholder="Ketik diskusi anda..."
maxLength={1000}
@@ -75,6 +79,6 @@ export default function ForumCreate() {
value={text}
onChangeText={setText}
/>
</ViewWrapper>
</OS_Wrapper>
);
}

View File

@@ -2,7 +2,7 @@ import {
BaseBox,
ButtonCustom,
CheckboxCustom,
NewWrapper,
OS_Wrapper,
StackCustom,
TextCustom,
} from "@/components";
@@ -54,7 +54,7 @@ export default function ForumSplash() {
};
return (
<NewWrapper>
<OS_Wrapper>
{/* <TextCustom bold>HIPMI Badung Connect</TextCustom> . */}
<BaseBox>
@@ -162,7 +162,7 @@ export default function ForumSplash() {
</ButtonCustom>
</StackCustom>
</BaseBox>
</NewWrapper>
</OS_Wrapper>
);
}

View File

@@ -1,66 +1,97 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import { BasicWrapper, Spacing, 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 HomeTabs from "@/screens/Home/HomeTabs";
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 { 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, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } 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);
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataJob();
checkVersion();
userData(token as string);
userData(token as string).catch((error) => {
console.log("[ERROR userData]", error?.message);
console.log("[ERROR userData Response]", error?.response?.data);
});
syncUnreadCount();
}, [user?.id, token]),
);
async function onLoadData() {
const response = await apiUser(user?.id as string);
console.log(
"[Profile ID]>>",
JSON.stringify(response?.data?.Profile?.id, null, 2),
);
setData(response.data);
try {
const response = await apiUser(user?.id as string);
setData(response.data);
} catch (error: any) {
console.log("[ERROR onLoadData]", error?.message);
console.log("[ERROR Response]", error?.response?.data);
// Set data tetap agar UI tidak stuck di loading
setData(null);
}
}
const onLoadDataJob = async () => {
try {
const response = await apiJobGetAll({
category: "beranda",
});
const result = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
.slice(0, 2);
setListData(result);
} catch (error) {
console.log("[ERROR]", error);
}
};
const checkVersion = async () => {
const response = await apiVersion();
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
try {
const response = await apiVersion();
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
} catch (error: any) {
console.log("[ERROR checkVersion]", error?.message);
}
};
const onRefresh = useCallback(() => {
setRefreshing(true);
onLoadData();
onLoadDataJob();
checkVersion();
setRefreshing(false);
}, []);
// if (user && user?.termsOfServiceAccepted === false) {
// console.log("User is not accept term service");
// return <Redirect href={`/terms-agreement`} />;
// }
if (data && data?.active === false) {
console.log("User is not active");
console.warn("User is not active");
return (
<BasicWrapper>
<Redirect href={`/waiting-room`} />
@@ -69,7 +100,7 @@ export default function Application() {
}
if (data && data?.Profile === null) {
console.log("Profile is null");
console.warn("Profile is null");
return (
<BasicWrapper>
<Redirect href={`/profile/create`} />
@@ -77,63 +108,95 @@ export default function Application() {
);
}
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
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
footerComponent={
<TabSection
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{
flexGrow: 1,
paddingInline: 10,
paddingBottom: paddingBottom + 80, // Space for tabs + safe area
}}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
keyboardShouldPersistTaps="handled"
>
<StackCustom>
<Home_ImageSection />
{data && data ? (
<Home_FeatureSection />
) : (
<View style={stylesHome.gridContainer}>
{Array.from({ length: 4 }).map((_, index) => (
<CustomSkeleton
key={index}
style={stylesHome.gridItem}
radius={50}
/>
))}
</View>
)}
{data ? (
<Home_BottomFeatureSection listData={listData} />
) : (
<CustomSkeleton height={150} />
)}
</StackCustom>
</ScrollView>
{/* Home Tabs di bawah */}
{data && data ? (
<HomeTabs
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,
})}
/>
}
>
<StackCustom>
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
Test Notif
</ButtonCustom> */}
<Home_ImageSection />
<Home_FeatureSection />
<Home_BottomFeatureSection />
</StackCustom>
</ViewWrapper>
) : (
<View style={{ height: 80 + paddingBottom, backgroundColor: MainColor.darkblue }} />
)}
</View>
</>
);
}

View File

@@ -4,80 +4,105 @@ import { TabsStyles } from "@/styles/tabs-styles";
import { Feather, FontAwesome6, Ionicons } from "@expo/vector-icons";
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
import { useLayoutEffect } from "react";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
export default function InvestmentTabsLayout() {
// const navigation = useNavigation();
function InvestmentTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
const navigation = useNavigation();
// const { from, category } = useLocalSearchParams<{
// from?: string;
// category?: string;
// }>();
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]);
// Atur header secara dinamis
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => (
<BackButtonFromNotification
from={from || ""}
category={category}
/>
),
});
}, [from, category, router, navigation]);
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Bursa",
tabBarIcon: ({ color }) => (
<Ionicons
name="bar-chart-outline"
color={color}
size={ICON_SIZE_SMALL}
/>
),
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
}}
/>
<Tabs.Screen
name="portofolio"
options={{
title: "Portofolio",
tabBarIcon: ({ color }) => (
<Feather name="pie-chart" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
<Tabs.Screen
name="my-holding"
options={{
title: "Saham Saya",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="hand-holding-dollar"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
<Tabs.Screen
name="transaction"
options={{
title: "Transaksi",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="money-bill-transfer"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Bursa",
tabBarIcon: ({ color }) => (
<Ionicons
name="bar-chart-outline"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
<Tabs.Screen
name="portofolio"
options={{
title: "Portofolio",
tabBarIcon: ({ color }) => (
<Feather name="pie-chart" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
<Tabs.Screen
name="my-holding"
options={{
title: "Saham Saya",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="hand-holding-dollar"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
<Tabs.Screen
name="transaction"
options={{
title: "Transaksi",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="money-bill-transfer"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
</Tabs>
</View>
);
}
export default function InvestmentTabsLayout() {
return <InvestmentTabsWrapper />;
}

View File

@@ -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
}
/>
),
}}
/>

View File

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

View File

@@ -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
}
/>
),
}}
/>

View File

@@ -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
}
/>
),
}}
/>

View File

@@ -1,37 +1,60 @@
/* 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";
import { Ionicons } from "@expo/vector-icons";
import { router, Tabs, useLocalSearchParams } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
import {
router,
Tabs,
useLocalSearchParams,
useNavigation
} from "expo-router";
import { useLayoutEffect } from "react";
export default function JobTabsLayout() {
const navigation = useNavigation();
OS_ANDROID_HEIGHT,
OS_ANDROID_PADDING_TOP,
OS_IOS_HEIGHT,
OS_IOS_PADDING_TOP,
} from "@/constants/constans-value";
function JobTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
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}>
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: OS_IOS_PADDING_TOP,
height: OS_IOS_HEIGHT,
},
android: {
borderTopWidth: 0,
paddingTop: OS_ANDROID_PADDING_TOP,
height: OS_ANDROID_HEIGHT + paddingBottom,
},
}),
header: () => (
<AppHeader
title="Job Vacancy"
left={
<BackButtonFromNotification
from={from || ""}
category={category}
/>
}
/>
),
}}
>
<Tabs.Screen
name="index"
options={{
@@ -56,6 +79,10 @@ export default function JobTabsLayout() {
}}
/>
</Tabs>
</>
</View>
);
}
export default function JobTabsLayout() {
return <JobTabsWrapper />;
}

View File

@@ -5,10 +5,11 @@ import {
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
OS_Wrapper,
Spacing,
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";
@@ -22,6 +23,7 @@ import {
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
import { PADDING_INLINE } from "@/constants/constans-value";
export default function JobDetailStatus() {
const { id, status } = useLocalSearchParams();
@@ -58,15 +60,20 @@ 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>
<OS_Wrapper >
{isLoadData ? (
<LoaderCustom />
) : (
@@ -77,7 +84,7 @@ export default function JobDetailStatus() {
(status === "draft" || status === "reject") && (
<ReportBox text={data?.catatan} />
)}
<Job_BoxDetailSection data={data} />
<Job_ButtonStatusSection
id={id as string}
@@ -90,7 +97,7 @@ export default function JobDetailStatus() {
<Spacing />
</>
)}
</ViewWrapper>
</OS_Wrapper>
<DrawerCustom
isVisible={openDrawer}

View File

@@ -1,198 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
DummyLandscapeImage,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job";
import {
deleteFileService,
uploadFileService,
} from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
import { Job_ScreenEdit } from "@/screens/Job/ScreenJobEdit";
export default function JobEdit() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>({
title: "",
content: "",
deskripsi: "",
});
const [isLoadData, setIsLoadData] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [imageUri, setImageUri] = useState<string | null>(null);
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handlerOnUpdate = async () => {
if (!data.title || !data.content || !data.deskripsi) {
Toast.show({
type: "info",
text1: "Info",
text2: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
let newImageId = "";
if (imageUri) {
const responseUploadImage = await uploadFileService({
imageUri: imageUri,
dirId: DIRECTORY_ID.job_image,
});
if (responseUploadImage.success) {
newImageId = responseUploadImage.data.id;
}
}
if (data?.imageId) {
const responseDeleteImage = await deleteFileService({
id: data.imageId,
});
if (!responseDeleteImage.success) {
console.log("[ERROR DELETE IMAGE]", responseDeleteImage.message);
}
}
const newData = {
title: data.title,
content: data.content,
deskripsi: data.deskripsi,
imageId: newImageId,
};
const response = await apiJobUpdateData({
id: id as string,
data: newData,
category: "edit",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => {
return (
<>
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnUpdate()}>
Update
</ButtonCustom>
<Spacing />
</>
);
};
return (
<ViewWrapper>
{isLoadData ? (
<LoaderCustom />
) : (
<StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
{imageUri ? (
<LandscapeFrameUploaded image={imageUri as any} />
) : (
<BaseBox>
<DummyLandscapeImage imageId={data?.imageId} />
</BaseBox>
)}
<ButtonCenteredOnly
onPress={() => {
pickImage({
setImageUri,
});
}}
icon="upload"
>
Upload
</ButtonCenteredOnly>
<Spacing />
<TextInputCustom
label="Judul Lowongan"
placeholder="Masukan Judul Lowongan Kerja"
required
value={data.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<TextAreaCustom
label="Syarat & Kualifikasi"
placeholder="Masukan Syarat & Kualifikasi Lowongan Kerja"
required
showCount
maxLength={1000}
value={data.content}
onChangeText={(value) => setData({ ...data, content: value })}
/>
<TextAreaCustom
label="Deskripsi Lowongan"
placeholder="Masukan Deskripsi Lowongan Kerja"
required
showCount
maxLength={1000}
value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
{buttonSubmit()}
</StackCustom>
)}
</ViewWrapper>
);
return <Job_ScreenEdit />;
}

View File

@@ -1,168 +1,5 @@
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
NewWrapper,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom
} from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import { apiJobCreate } from "@/service/api-client/api-job";
import { uploadFileService } from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
import { Job_ScreenCreate } from "@/screens/Job/ScreenJobCreate";
export default function JobCreate() {
const nextUrl = "/(application)/(user)/job/(tabs)/status?status=review";
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [image, setImage] = useState<string | null>(null);
const [data, setData] = useState({
title: "",
content: "",
deskripsi: "",
authorId: "",
});
const handlerOnSubmit = async () => {
let imageId = "";
const newData = {
title: data.title,
content: data.content,
deskripsi: data.deskripsi,
authorId: user?.id,
imageId: "",
};
if (!data.title || !data.content || !data.deskripsi || !user?.id) {
Toast.show({
type: "info",
text1: "Info",
text2: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
if (image === null || !image) {
const response = await apiJobCreate(newData);
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: "Lowongan berhasil dibuat",
});
router.replace(nextUrl);
}
return;
}
const responseUploadImage = await uploadFileService({
imageUri: image,
dirId: DIRECTORY_ID.job_image,
});
if (responseUploadImage.success) {
imageId = responseUploadImage.data.id;
}
const fixData = {
...newData,
imageId: imageId,
};
const response = await apiJobCreate(fixData);
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: "Lowongan berhasil dibuat",
});
router.replace(nextUrl);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => {
return (
<>
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
</>
);
};
return (
<NewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
{/* <BaseBox>
<Image
source={image ? { uri: image } : DUMMY_IMAGE.dummy_image}
style={{ width: "100%", height: 200 }}
/>
</BaseBox> */}
<LandscapeFrameUploaded image={image as string} />
<ButtonCenteredOnly
onPress={() => {
// router.push("/(application)/(image)/take-picture/123");
pickImage({
setImageUri: setImage,
});
}}
icon="upload"
>
Upload
</ButtonCenteredOnly>
<Spacing />
<TextInputCustom
label="Judul Lowongan"
placeholder="Masukan Judul Lowongan Kerja"
required
value={data.title}
onChangeText={(value) => setData({ ...data, title: value })}
/>
<TextAreaCustom
label="Syarat & Kualifikasi"
placeholder="Masukan Syarat & Kualifikasi Lowongan Kerja"
required
showCount
maxLength={1000}
value={data.content}
onChangeText={(value) => setData({ ...data, content: value })}
/>
<TextAreaCustom
label="Deskripsi Lowongan"
placeholder="Masukan Deskripsi Lowongan Kerja"
required
showCount
maxLength={1000}
value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
</StackCustom>
</NewWrapper>
);
return <Job_ScreenCreate />;
}

View File

@@ -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,
},
});

View File

@@ -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 />
</>
);
}

View File

@@ -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> */}
</>
);

View File

@@ -1,365 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
ActionIcon,
AvatarComp,
BaseBox,
ButtonCenteredOnly,
CenterCustom,
Grid,
InformationBox,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
} from "@/service/api-client/api-master";
import {
IMasterBidangBisnis,
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import pickImage from "@/utils/pickImage";
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { Avatar } from "react-native-paper";
import { ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate";
export default function PortofolioCreate() {
const { id } = useLocalSearchParams();
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [inputValue, setInputValue] = useState<string>("");
const [data, setData] = useState({
namaBisnis: "",
masterBidangBisnisId: "",
alamatKantor: "",
tlpn: "",
deskripsi: "",
});
const [imageUri, setImageUri] = useState<string | null>(null);
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
const [subBidangBisnis, setSubBidangBisnis] = useState<
IMasterSubBidangBisnis[]
>([]);
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState([
{
id: "",
},
]);
const [dataMedsos, setDataMedsos] = useState({
facebook: "",
twitter: "",
instagram: "",
youtube: "",
tiktok: "",
});
const [isLoadingCreate, setIsLoadingCreate] = useState(false);
function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber);
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country);
}
useFocusEffect(
useCallback(() => {
onLoadMaster();
onLoadMasterSubBidangBisnis();
}, [])
);
const onLoadMaster = async () => {
try {
const response = await apiMasterBidangBisnis();
setBidangBisnis(response.data);
} catch (error) {
setBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
const onLoadMasterSubBidangBisnis = async () => {
try {
const response = await apiMasterSubBidangBisnis({});
setSubBidangBisnis(response.data);
} catch (error) {
setSubBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
const handlerSelectedSubBidang = ({ id }: { id: string }) => {
const selectedList = subBidangBisnis?.filter(
(item) => (item?.masterBidangBisnisId as any) === id
);
setSelectedSubBidang(selectedList as any[]);
};
return (
<NewWrapper
footerComponent={
<Portofolio_ButtonCreate
id={id as string}
data={data}
dataMedsos={dataMedsos}
imageUri={imageUri}
subBidangSelected={listSubBidangSelected}
isLoadingCreate={isLoadingCreate}
setIsLoadingCreate={setIsLoadingCreate}
/>
}
>
{/* <TextCustom>Portofolio Create {id}</TextCustom> */}
<StackCustom gap={"xs"}>
<InformationBox text="Lengkapi data bisnis anda." />
<TextInputCustom
required
label="Nama Bisnis"
placeholder="Masukkan nama bisnis"
onChangeText={(value: any) => setData({ ...data, namaBisnis: value })}
/>
<SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterBidangBisnisId}
onChange={(value) => {
const isSameBidang = data.masterBidangBisnisId === value;
if (!isSameBidang) {
setListSubBidangSelected([{ id: "" }]);
}
setData({ ...(data as any), masterBidangBisnisId: value });
handlerSelectedSubBidang({ id: value as string });
}}
/>
{listSubBidangSelected.map((item, index) => (
<SelectCustom
key={index}
disabled={data.masterBidangBisnisId === ""}
label="Sub Bidang Usaha"
required
data={_.map(selectedSubBidang as any)
.filter((option: any) => {
const selectedValues = listSubBidangSelected.map((s) => s.id);
return (
option.id === item.id || // biarkan tetap muncul kalau ini valuenya sendiri
!selectedValues.includes(option.id)
);
})
.map((e: any) => ({
value: e.id,
label: e.name,
}))}
value={item.id || null}
onChange={(value) => {
const list = _.clone(listSubBidangSelected);
list[index].id = value as any;
setListSubBidangSelected(list);
}}
/>
))}
<CenterCustom>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<ActionIcon
disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => {
setListSubBidangSelected([
...listSubBidangSelected,
{ id: "" },
]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
const list = _.clone(listSubBidangSelected);
list.pop();
setListSubBidangSelected(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
{/* <SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={null}
onChange={(value) => {
setData({ ...(data as any), masterBidangBisnisId: value });
handlerSelectedSubBidang({ id: value as string });
}}
/> */}
{/* <ButtonCenteredOnly
onPress={() => {
setListSubBidangSelected([...listSubBidangSelected, { id: "" }]);
}}
>
Tambah Pilihan
</ButtonCenteredOnly>
<Spacing /> */}
{/* <TextCustom>{JSON.stringify(bidangBisnis, null, 2)}</TextCustom> */}
<View>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TextCustom semiBold style={{ color: MainColor.white_gray }}>
Nomor Telepon
</TextCustom>
<Text style={{ color: "red" }}> *</Text>
</View>
<Spacing height={5} />
<PhoneInput
value={inputValue}
onChangePhoneNumber={handleInputValue}
selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry}
defaultCountry="ID"
placeholder="xxx-xxx-xxx"
/>
</View>
<Spacing />
<TextInputCustom
required
label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis"
onChangeText={(value: any) =>
setData({ ...data, alamatKantor: value })
}
/>
<TextAreaCustom
label="Deskripsi Bisnis"
placeholder="Masukkan deskripsi bisnis"
value={data.deskripsi}
onChangeText={(value: any) => setData({ ...data, deskripsi: value })}
autosize
minRows={2}
maxRows={5}
required
showCount
maxLength={1000}
/>
<Spacing />
{/* Logo */}
<InformationBox text="Upload logo bisnis anda untuk di tampilaka pada portofolio." />
<CenterCustom>
<Avatar.Image
source={imageUri ? { uri: imageUri } : DUMMY_IMAGE.dummy_image}
size={200}
/>
</CenterCustom>
<Spacing />
<ButtonCenteredOnly
icon="upload"
onPress={() => {
pickImage({
setImageUri,
});
}}
>
Upload
</ButtonCenteredOnly>
<Spacing height={40} />
{/* Social Media */}
<InformationBox text="Isi hanya pada sosial media yang anda miliki." />
<TextInputCustom
label="Tiktok"
placeholder="Masukkan username tiktok"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, tiktok: value })
}
/>
<TextInputCustom
label="Facebook"
placeholder="Masukkan username facebook"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, facebook: value })
}
/>
<TextInputCustom
label="Instagram"
placeholder="Masukkan username instagram"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, instagram: value })
}
/>
<TextInputCustom
label="Twitter"
placeholder="Masukkan username twitter"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, twitter: value })
}
/>
<TextInputCustom
label="Youtube"
placeholder="Masukkan username youtube"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, youtube: value })
}
/>
{/* <Spacing /> */}
</StackCustom>
</NewWrapper>
);
return <ScreenPortofolioCreate />;
}

View File

@@ -3,7 +3,7 @@ import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
ViewWrapper
OS_Wrapper
} from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
@@ -126,7 +126,7 @@ export default function PortofolioEditLogo() {
return (
<>
<ViewWrapper footerComponent={buttonFooter}>
<OS_Wrapper footerComponent={buttonFooter}>
<BaseBox
style={{
alignItems: "center",
@@ -146,7 +146,7 @@ export default function PortofolioEditLogo() {
>
Upload
</ButtonCenteredOnly>
</ViewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -1,8 +1,8 @@
import {
BoxButtonOnFooter,
ButtonCustom,
OS_Wrapper,
TextInputCustom,
ViewWrapper,
} from "@/components";
import {
apiGetOnePortofolio,
@@ -91,7 +91,11 @@ export default function PortofolioEditSocialMedia() {
return (
<>
<ViewWrapper footerComponent={buttonFooter}>
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
footerComponent={buttonFooter}
>
<TextInputCustom
value={data.tiktok}
onChangeText={(value) => setData({ ...data, tiktok: value })}
@@ -122,7 +126,7 @@ export default function PortofolioEditSocialMedia() {
label="Youtube"
placeholder="Masukkan youtube"
/>
</ViewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -4,7 +4,8 @@ import {
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
NewWrapper,
OS_Wrapper,
PhoneInputCustom,
SelectCustom,
Spacing,
StackCustom,
@@ -15,6 +16,11 @@ import {
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import {
DEFAULT_COUNTRY,
type CountryData,
COUNTRIES,
} from "@/constants/countries";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
@@ -32,7 +38,6 @@ import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { ActivityIndicator } from "react-native-paper";
import Toast from "react-native-toast-message";
@@ -59,8 +64,9 @@ export default function PortofolioEdit() {
const { id } = useLocalSearchParams();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<any>({});
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [selectedCountry, setSelectedCountry] =
useState<CountryData>(DEFAULT_COUNTRY);
const [bidangBisnis, setBidangBisnis] = useState<
IMasterBidangBisnis[] | null
>(null);
@@ -72,12 +78,42 @@ export default function PortofolioEdit() {
IListSubBidangSelected[]
>([]);
function handleInputValue(phoneNumber: string) {
setData({ ...data, tlpn: phoneNumber });
function handlePhoneChange(phone: string) {
setPhoneNumber(phone);
// Format phone number for API
const callingCode = selectedCountry.callingCode;
let fixNumber = phone.replace(/\s+/g, "").replace(/^0+/, "");
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
function handleSelectedCountry(country: ICountry) {
function handleCountryChange(country: CountryData) {
setSelectedCountry(country);
// Re-format with new country code
const callingCode = country.callingCode;
let fixNumber = phoneNumber.replace(/\s+/g, "").replace(/^0+/, "");
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
const onLoadMasterBidang = async () => {
@@ -122,8 +158,27 @@ export default function PortofolioEdit() {
const response = await apiGetOnePortofolio({ id: id });
if (response.success) {
const fixNumber = response.data.tlpn.replace("62", "");
setData({ ...response.data, tlpn: fixNumber });
// Extract phone number without country code for display
const fullNumber = response.data.tlpn;
let displayNumber = fullNumber;
let detectedCountry = DEFAULT_COUNTRY;
// Try to detect country from calling code
for (const country of COUNTRIES) {
if (fullNumber.startsWith(country.callingCode)) {
detectedCountry = country;
displayNumber = fullNumber.substring(country.callingCode.length);
break;
}
}
setSelectedCountry(detectedCountry);
// Remove leading zero if present
displayNumber = displayNumber.replace(/^0+/, "");
setPhoneNumber(displayNumber);
setData({ ...response.data, tlpn: displayNumber });
// Cek apakah ada sub bidang bisnis yang terpilih
const prevSubBidang = response.data.Portofolio_BidangDanSubBidangBisnis;
@@ -244,15 +299,11 @@ export default function PortofolioEdit() {
}
const handleSubmitUpdate = async () => {
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
let fixNumber = data.tlpn.replace(/\s+/g, "").replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
const newData: IFormData = {
id_Portofolio: data.id_Portofolio,
namaBisnis: data.namaBisnis,
alamatKantor: data.alamatKantor,
tlpn: realNumber,
tlpn: data.tlpn, // Already formatted by PhoneInputCustom
deskripsi: data.deskripsi,
masterBidangBisnisId: data.masterBidangBisnisId,
subBidang: listSubBidangSelected,
@@ -317,162 +368,159 @@ export default function PortofolioEdit() {
</BoxButtonOnFooter>
);
if (!bidangBisnis || !subBidangBisnis) {
return (
<>
<NewWrapper>
<ListSkeletonComponent height={80} />
</NewWrapper>
</>
);
}
return (
<>
<NewWrapper footerComponent={buttonUpdate}>
<StackCustom gap={"xs"}>
<TextInputCustom
required
label="Nama Bisnis"
placeholder="Masukkan nama bisnis"
value={data.namaBisnis}
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: any) => {
handleBidangBisnisChange(value);
}}
/>
{listSubBidangSelected.map((item, index) => {
// Filter data untuk select sub bidang, menghilangkan yang sudah dipilih kecuali untuk item ini sendiri
const selectedIds = listSubBidangSelected
.filter((_, i) => i !== index)
.map((s) => s.MasterSubBidangBisnis?.id)
.filter((id) => id); // Filter hanya yang memiliki id (tidak kosong)
const availableSubBidangOptions = (selectedSubBidang || [])
.filter((sub: any) => {
// Tampilkan jika ini adalah opsi yang dipilih saat ini atau belum dipilih di sub bidang lainnya
return (
sub.id === item.MasterSubBidangBisnis?.id ||
!selectedIds.includes(sub.id)
);
})
.map((sub: any) => ({
value: sub.id,
label: sub.name,
}));
return (
<SelectCustom
key={index}
label="Sub Bidang Usaha"
required
data={availableSubBidangOptions}
value={item.MasterSubBidangBisnis?.id || null}
onChange={(value: any) => {
handleSubBidangChange(value, index);
}}
/>
);
})}
<CenterCustom>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
>
<ActionIcon
disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => {
handleAddSubBidang();
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
handleRemoveSubBidang(listSubBidangSelected.length - 1);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
<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={data.tlpn}
onChangePhoneNumber={handleInputValue}
selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry}
defaultCountry="ID"
placeholder="xxx-xxx-xxx"
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
footerComponent={buttonUpdate}
>
{!bidangBisnis || !subBidangBisnis ? (
<ListSkeletonComponent height={80} />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom
required
label="Nama Bisnis"
placeholder="Masukkan nama bisnis"
value={data.namaBisnis}
onChangeText={(value: any) =>
setData({ ...data, namaBisnis: value })
}
/>
</View>
<Spacing />
<TextInputCustom
required
label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis"
value={data.alamatKantor}
onChangeText={(value: any) =>
setData({ ...data, alamatKantor: value })
}
/>
<SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterBidangBisnisId}
onChange={(value: any) => {
handleBidangBisnisChange(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 />
</StackCustom>
</NewWrapper>
{listSubBidangSelected.map((item, index) => {
// Filter data untuk select sub bidang, menghilangkan yang sudah dipilih kecuali untuk item ini sendiri
const selectedIds = listSubBidangSelected
.filter((_, i) => i !== index)
.map((s) => s.MasterSubBidangBisnis?.id)
.filter((id) => id); // Filter hanya yang memiliki id (tidak kosong)
const availableSubBidangOptions = (selectedSubBidang || [])
.filter((sub: any) => {
// Tampilkan jika ini adalah opsi yang dipilih saat ini atau belum dipilih di sub bidang lainnya
return (
sub.id === item.MasterSubBidangBisnis?.id ||
!selectedIds.includes(sub.id)
);
})
.map((sub: any) => ({
value: sub.id,
label: sub.name,
}));
return (
<SelectCustom
key={index}
label="Sub Bidang Usaha"
required
data={availableSubBidangOptions}
value={item.MasterSubBidangBisnis?.id || null}
onChange={(value: any) => {
handleSubBidangChange(value, index);
}}
/>
);
})}
<CenterCustom>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
>
<ActionIcon
disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => {
handleAddSubBidang();
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
handleRemoveSubBidang(listSubBidangSelected.length - 1);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
<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} />
<PhoneInputCustom
value={phoneNumber}
onChangePhoneNumber={handlePhoneChange}
selectedCountry={selectedCountry}
onChangeCountry={handleCountryChange}
placeholder="xxx-xxx-xxx"
/>
</View>
<Spacing />
<TextInputCustom
required
label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis"
value={data.alamatKantor}
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 />
</StackCustom>
)}
</OS_Wrapper>
</>
);
}

View File

@@ -1,16 +1,18 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
LoaderCustom,
OS_Wrapper,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import LeftButtonCustom from "@/components/Button/BackButton";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
@@ -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>
<OS_Wrapper>
{!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}
/>
@@ -112,7 +125,7 @@ export default function Portofolio() {
<Spacing />
</StackCustom>
)}
</ViewWrapper>
</OS_Wrapper>
{/* Drawer Komponen Eksternal */}
<DrawerCustom
@@ -135,10 +148,12 @@ export default function Portofolio() {
closeDrawer={() => setOpenDrawerLocation(false)}
height={"auto"}
>
<DummyLandscapeImage
height={200}
imageId={data?.BusinessMaps?.imageId}
/>
{data?.BusinessMaps?.imageId && (
<DummyLandscapeImage
height={200}
imageId={data?.BusinessMaps?.imageId}
/>
)}
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView

View File

@@ -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" }} /> */}

View File

@@ -3,13 +3,13 @@ import {
BadgeCustom,
ClickableCustom,
Divider,
OS_Wrapper,
SelectCustom,
TextCustom,
} from "@/components";
import ListEmptyComponent from "@/components/_ShareComponent/ListEmptyComponent";
import ListLoaderFooterComponent from "@/components/_ShareComponent/ListLoaderFooterComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { usePaginatedApi } from "@/hooks/use-paginated-api";
@@ -120,7 +120,7 @@ export default function ProfileBlockedList() {
return (
<>
<NewWrapper
<OS_Wrapper
// headerComponent={renderHeader()}
listData={listData}
renderItem={renderItem}

View File

@@ -5,7 +5,7 @@ import {
BoxButtonOnFooter,
BoxWithHeaderSection,
ButtonCustom,
NewWrapper,
OS_Wrapper,
StackCustom,
TextCustom,
} from "@/components";
@@ -46,7 +46,7 @@ export default function ProfileDetailBlocked() {
return (
<>
<NewWrapper
<OS_Wrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
@@ -86,7 +86,7 @@ export default function ProfileDetailBlocked() {
</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
</NewWrapper>
</OS_Wrapper>
</>
);
}

View File

@@ -1,11 +1,12 @@
import {
ButtonCustom,
OS_Wrapper,
SelectCustom,
StackCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import BoxButtonOnFooter from "@/components/Box/BoxButtonOnFooter";
import { PADDING_INLINE } from "@/constants/constans-value";
import { apiProfile, apiUpdateProfile } from "@/service/api-client/api-profile";
import { IProfile } from "@/types/Type-Profile";
import { router, useLocalSearchParams } from "expo-router";
@@ -70,7 +71,9 @@ export default function ProfileEdit() {
};
return (
<ViewWrapper
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={handleUpdate}>
@@ -119,6 +122,6 @@ export default function ProfileEdit() {
}}
/>
</StackCustom>
</ViewWrapper>
</OS_Wrapper>
);
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { OS_Wrapper, 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>
<OS_Wrapper
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>
</OS_Wrapper>
{/* Drawer Komponen Eksternal */}
<DrawerCustom

View File

@@ -3,8 +3,8 @@ import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
OS_Wrapper,
} from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
@@ -127,7 +127,7 @@ export default function UpdateBackgroundProfile() {
);
return (
<ViewWrapper footerComponent={buttonFooter}>
<OS_Wrapper footerComponent={buttonFooter}>
<BaseBox
style={{ alignItems: "center", justifyContent: "center", height: 250 }}
>
@@ -144,6 +144,6 @@ export default function UpdateBackgroundProfile() {
>
Update
</ButtonCenteredOnly>
</ViewWrapper>
</OS_Wrapper>
);
}

View File

@@ -3,8 +3,8 @@ import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
OS_Wrapper,
} from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
@@ -125,7 +125,7 @@ export default function UpdatePhotoProfile() {
);
return (
<ViewWrapper footerComponent={buttonFooter}>
<OS_Wrapper footerComponent={buttonFooter}>
<BaseBox
style={{ alignItems: "center", justifyContent: "center", height: 250 }}
>
@@ -143,6 +143,6 @@ export default function UpdatePhotoProfile() {
>
Upload
</ButtonCenteredOnly>
</ViewWrapper>
</OS_Wrapper>
);
}

View File

@@ -1,47 +1,43 @@
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={{
header: () => (
<AppHeader title="Tambah Profil" showBack={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>
</>

View File

@@ -2,16 +2,17 @@ import {
BaseBox,
ButtonCenteredOnly,
ButtonCustom,
OS_Wrapper,
SelectCustom,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import BoxButtonOnFooter from "@/components/Box/BoxButtonOnFooter";
import InformationBox from "@/components/Box/InformationBox";
import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { PADDING_INLINE } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { apiCreateProfile } from "@/service/api-client/api-profile";
import { apiValidationEmail } from "@/service/api-client/api-validation";
@@ -155,7 +156,11 @@ export default function CreateProfile() {
);
return (
<ViewWrapper footerComponent={footerComponent}>
<OS_Wrapper
enableKeyboardHandling
contentPaddingBottom={250}
footerComponent={footerComponent}
>
<StackCustom>
<InformationBox text="Upload foto profile anda." />
<View style={{ alignItems: "center" }}>
@@ -241,6 +246,6 @@ export default function CreateProfile() {
/>
<Spacing />
</StackCustom>
</ViewWrapper>
</OS_Wrapper>
);
}

View File

@@ -4,64 +4,86 @@ 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";
export default function VotingTabsLayout() {
const navigation = useNavigation();
import { router, Tabs, useLocalSearchParams } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
function VotingTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
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.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
header: () => (
<AppHeader
title="Voting"
left={
<BackButtonFromNotification
from={from || ""}
category={category}
/>
}
/>
),
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="contribution"
options={{
title: "Kontribusi",
tabBarIcon: ({ color }) => <IconContribution color={color} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Riwayat",
tabBarIcon: ({ color }) => <IconHistory color={color} />,
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="contribution"
options={{
title: "Kontribusi",
tabBarIcon: ({ color }) => <IconContribution color={color} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Riwayat",
tabBarIcon: ({ color }) => <IconHistory color={color} />,
}}
/>
</Tabs>
</View>
);
}
export default function VotingTabsLayout() {
return <VotingTabsWrapper />;
}

View File

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

View File

@@ -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)} />
}
/>
),
}}
/>

View File

@@ -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)} />
}
/>
),
}}
/>

View File

@@ -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)} />
}
/>
),
}}
/>

View File

@@ -3,7 +3,7 @@ import {
BoxButtonOnFooter,
ButtonCustom,
InformationBox,
NewWrapper,
OS_Wrapper,
StackCustom
} from "@/components";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
@@ -82,7 +82,7 @@ export default function WaitingRoom() {
return (
<>
<NewWrapper
<OS_Wrapper
footerComponent={logoutButton()}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={handleCheck} />
@@ -103,7 +103,7 @@ Silakan tunggu beberapa saat. Untuk memperbarui status, tarik layar ke bawah."
Check
</ButtonCenteredOnly> */}
</StackCustom>
</NewWrapper>
</OS_Wrapper>
</>
);
}

View File

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

View File

@@ -6,6 +6,7 @@ import {
StackCustom,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
import NavbarMenu from "@/components/Drawer/NavbarMenu";
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
@@ -17,6 +18,7 @@ import {
ICON_SIZE_XLARGE,
} from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import AdminNotificationBell from "@/screens/Admin/AdminNotificationBell";
import {
adminListMenu,
@@ -34,12 +36,28 @@ import { useState } from "react";
export default function AdminLayout() {
const [openDrawerNavbar, setOpenDrawerNavbar] = useState(false);
const [openDrawerUser, setOpenDrawerUser] = useState(false);
// const [user, setUser] = useState(null);
const { logout, user } = useAuth();
console.log("[USER LAYOUT]", JSON.stringify(user, null, 2));
const headerLeft = () => (
<Ionicons
name="menu"
size={ICON_SIZE_XLARGE}
color={MainColor.white}
onPress={() => setOpenDrawerNavbar(true)}
/>
);
const headerRight = () => (
<FontAwesome6
name="circle-user"
size={ICON_SIZE_MEDIUM}
color={MainColor.white}
onPress={() => setOpenDrawerUser(true)}
/>
);
return (
<>
<Stack
@@ -51,20 +69,33 @@ export default function AdminLayout() {
contentStyle: {
borderBottomColor: AccentColor.blue,
},
headerLeft: () => (
<Ionicons
name="menu"
size={ICON_SIZE_XLARGE}
color={MainColor.white}
onPress={() => setOpenDrawerNavbar(true)}
/>
),
headerRight: () => (
<FontAwesome6
name="circle-user"
size={ICON_SIZE_MEDIUM}
color={MainColor.white}
onPress={() => setOpenDrawerUser(true)}
// headerLeft: () => (
// <Ionicons
// name="menu"
// size={ICON_SIZE_XLARGE}
// color={MainColor.white}
// onPress={() => setOpenDrawerNavbar(true)}
// />
// ),
// headerRight: () => (
// <FontAwesome6
// name="circle-user"
// size={ICON_SIZE_MEDIUM}
// color={MainColor.white}
// onPress={() => setOpenDrawerUser(true)}
// />
// ),
header: () => (
<AppHeader
title="HIPMI DASHBOARD"
showBack={false}
left={headerLeft()}
right={headerRight()}
/>
),
}}

View File

@@ -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 />;
}

View File

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

View File

@@ -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 />;
}

View File

@@ -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 />;
}

View File

@@ -9,32 +9,14 @@ import {
ViewWrapper,
} from "@/components";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import API_IMAGE from "@/constants/api-storage";
import { MapMarker, MapsV2Custom } from "@/components/Map/MapsV2Custom";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiMapsGetAll } from "@/service/api-client/api-maps";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { View } from "react-native";
import MapView, { Marker } from "react-native-maps";
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
height: 300,
};
export interface LocationItem {
id: string | number;
latitude: number;
longitude: number;
name: string;
imageId?: string;
}
export default function AdminMaps() {
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
@@ -72,74 +54,30 @@ export default function AdminMaps() {
}
};
const markers: MapMarker[] = list?.map((item) => ({
id: item.id,
coordinate: [item.longitude, item.latitude] as [number, number],
imageId: item.Portofolio?.logoId,
onSelected: () => {
setOpenDrawer(true);
setSelected({
id: item?.id,
bidangBisnis: item?.Portofolio?.MasterBidangBisnis?.name,
nomorTelepon: item?.Portofolio?.tlpn,
alamatBisnis: item?.Portofolio?.alamatKantor,
namePin: item?.namePin,
imageId: item?.imageId,
portofolioId: item?.Portofolio?.id,
latitude: item?.latitude,
longitude: item?.longitude,
});
},
})) || [];
return (
<>
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
{/* <MapCustom height={"100%"} /> */}
<View style={{ flex: 1 }}>
{loadList ? (
<MapView
initialRegion={defaultRegion}
style={{
width: "100%",
height: "100%",
}}
/>
) : (
<MapView
initialRegion={defaultRegion}
style={{
width: "100%",
height: "100%",
}}
>
{list?.map((item: any, index: number) => {
return (
<Marker
key={item?.id}
coordinate={{
latitude: item?.latitude,
longitude: item?.longitude,
}}
title={item?.namePin}
onPress={() => {
setOpenDrawer(true);
setSelected({
id: item?.id,
bidangBisnis:
item?.Portofolio?.MasterBidangBisnis?.name,
nomorTelepon: item?.Portofolio?.tlpn,
alamatBisnis: item?.Portofolio?.alamatKantor,
namePin: item?.namePin,
imageId: item?.imageId,
portofolioId: item?.Portofolio?.id,
latitude: item?.latitude,
longitude: item?.longitude,
});
}}
// Gunakan gambar kustom jika tersedia
>
<View>
<Image
source={{
uri: API_IMAGE.GET({
fileId: item?.Portofolio?.logoId,
}),
}}
style={{
width: 30,
height: 30,
borderRadius: 100,
borderWidth: 1,
}}
/>
</View>
</Marker>
);
})}
</MapView>
)}
</View>
<MapsV2Custom markers={markers} />
</ViewWrapper>
<DrawerCustom
@@ -147,7 +85,9 @@ export default function AdminMaps() {
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<DummyLandscapeImage height={200} imageId={selected.imageId} />
{selected.imageId && (
<DummyLandscapeImage height={200} imageId={selected.imageId} />
)}
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView

View File

@@ -3,9 +3,9 @@ import {
BoxButtonOnFooter,
ButtonCustom,
LoaderCustom,
OS_Wrapper,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
@@ -76,7 +76,7 @@ export default function AdminUserAccessDetail() {
return (
<>
<ViewWrapper
<OS_Wrapper
headerComponent={<AdminBackButtonAntTitle title={`Detail User`} />}
footerComponent={
data && (
@@ -108,7 +108,7 @@ export default function AdminUserAccessDetail() {
))}
</StackCustom>
)}
</ViewWrapper>
</OS_Wrapper>
</>
);
}

View File

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

View File

@@ -5,6 +5,7 @@
"name": "hipmi-mobile",
"dependencies": {
"@expo/vector-icons": "^15.0.2",
"@maplibre/maplibre-react-native": "^10.4.2",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-firebase/app": "^23.7.0",
@@ -14,7 +15,6 @@
"@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.10",
"@rnmapbox/maps": "^10.2.7",
"@types/lodash": "^4.17.20",
"@types/react-native-vector-icons": "^6.4.18",
"axios": "^1.11.0",
@@ -41,6 +41,7 @@
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.9",
"libphonenumber-js": "^1.12.40",
"lodash": "^4.17.21",
"moti": "^0.30.0",
"react": "19.1.0",
@@ -580,6 +581,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@maplibre/maplibre-react-native": ["@maplibre/maplibre-react-native@10.4.2", "", { "dependencies": { "@turf/distance": "^7.1.0", "@turf/helpers": "^7.1.0", "@turf/length": "^7.1.0", "@turf/nearest-point-on-line": "^7.1.0", "debounce": "^2.2.0" }, "peerDependencies": { "@expo/config-plugins": ">=7", "@types/geojson": "^7946.0.0", "@types/react": ">=16.6.1", "react": ">=16.6.1", "react-native": ">=0.59.9" }, "optionalPeers": ["@expo/config-plugins", "@types/geojson", "@types/react"] }, "sha512-5qAfaEe66eMXyILklm2DMHwyaXwXxsZWVop4BqfU7AyTg13LHAcaMmLJNJ3jPkMtiJvjH2m8ywGnobdIg2I0lg=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
"@motionone/dom": ["@motionone/dom@10.12.0", "", { "dependencies": { "@motionone/animation": "^10.12.0", "@motionone/generators": "^10.12.0", "@motionone/types": "^10.12.0", "@motionone/utils": "^10.12.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw=="],
@@ -742,8 +745,6 @@
"@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="],
"@rnmapbox/maps": ["@rnmapbox/maps@10.2.10", "", { "dependencies": { "@turf/along": "6.5.0", "@turf/distance": "6.5.0", "@turf/helpers": "6.5.0", "@turf/length": "6.5.0", "@turf/nearest-point-on-line": "6.5.0", "@types/geojson": "^7946.0.7", "debounce": "^2.2.0" }, "peerDependencies": { "expo": ">=47.0.0", "mapbox-gl": "^2.9.0", "react": ">=17.0.0", "react-dom": ">= 17.0.0", "react-native": ">=0.69" }, "optionalPeers": ["expo", "mapbox-gl", "react-dom"] }, "sha512-OfjW0rHp5bUWfzBo5fZ7qdKwAzGoocXYTsSssSPVMxZ2Y7axuhcbmsO5bV6gg+BJs5RwEsghzwTIoGydBNUClA=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="],
@@ -764,29 +765,17 @@
"@tsconfig/node18": ["@tsconfig/node18@18.2.6", "", {}, "sha512-eAWQzAjPj18tKnDzmWstz4OyWewLUNBm9tdoN9LayzoboRktYx3Enk1ZXPmThj55L7c4VWYq/Bzq0A51znZfhw=="],
"@turf/along": ["@turf/along@6.5.0", "", { "dependencies": { "@turf/bearing": "^6.5.0", "@turf/destination": "^6.5.0", "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-LLyWQ0AARqJCmMcIEAXF4GEu8usmd4Kbz3qk1Oy5HoRNpZX47+i5exQtmIWKdqJ1MMhW26fCTXgpsEs5zgJ5gw=="],
"@turf/distance": ["@turf/distance@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@turf/invariant": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-9drWgd46uHPPyzgrcRQLgSvdS/SjVlQ6ZIBoRQagS5P2kSjUbcOXHIMeOSPwfxwlKhEtobLyr+IiR2ns1TfF8w=="],
"@turf/bbox": ["@turf/bbox@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-D5ErVWtfQbEPh11yzI69uxqrcJmbPU/9Y59f1uTapgwAwQHQztDWgsYpnL3ns8r1GmPWLP8sGJLVTIk2TZSiYA=="],
"@turf/helpers": ["@turf/helpers@7.3.4", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g=="],
"@turf/bearing": ["@turf/bearing@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A=="],
"@turf/invariant": ["@turf/invariant@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-88Eo4va4rce9sNZs6XiMJowWkikM3cS2TBhaCKlU+GFHdNf8PFEpiU42VDU8q5tOF6/fu21Rvlke5odgOGW4AQ=="],
"@turf/destination": ["@turf/destination@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ=="],
"@turf/length": ["@turf/length@7.3.4", "", { "dependencies": { "@turf/distance": "7.3.4", "@turf/helpers": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Dg1GnQ/B2go5NIWXt91N4L7XTjIgIWCftBSYIXkrpIM7QGjItzglek0Z5caytvb8ZRWXzZOGs8//+Q5we91WuQ=="],
"@turf/distance": ["@turf/distance@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg=="],
"@turf/meta": ["@turf/meta@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw=="],
"@turf/helpers": ["@turf/helpers@6.5.0", "", {}, "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw=="],
"@turf/invariant": ["@turf/invariant@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg=="],
"@turf/length": ["@turf/length@6.5.0", "", { "dependencies": { "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-5pL5/pnw52fck3oRsHDcSGrj9HibvtlrZ0QNy2OcW8qBFDNgZ4jtl6U7eATVoyWPKBHszW3dWETW+iLV7UARig=="],
"@turf/line-intersect": ["@turf/line-intersect@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/line-segment": "^6.5.0", "@turf/meta": "^6.5.0", "geojson-rbush": "3.x" } }, "sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA=="],
"@turf/line-segment": ["@turf/line-segment@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw=="],
"@turf/meta": ["@turf/meta@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA=="],
"@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@6.5.0", "", { "dependencies": { "@turf/bearing": "^6.5.0", "@turf/destination": "^6.5.0", "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/line-intersect": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg=="],
"@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@7.3.4", "", { "dependencies": { "@turf/distance": "7.3.4", "@turf/helpers": "7.3.4", "@turf/invariant": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-DQrP3lRju83rIXFN68tUEpc7ki/eRwdwBkK2CTT4RAcyCxbcH2NGJPQv8dYiww/Ar77u1WLVn+aINXZH904dWw=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
@@ -1482,8 +1471,6 @@
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"geojson-rbush": ["geojson-rbush@3.2.0", "", { "dependencies": { "@turf/bbox": "*", "@turf/helpers": "6.x", "@turf/meta": "6.x", "@types/geojson": "7946.0.8", "rbush": "^3.0.1" } }, "sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
@@ -1786,6 +1773,8 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"libphonenumber-js": ["libphonenumber-js@1.12.40", "", {}, "sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg=="],
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
@@ -2082,14 +2071,10 @@
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"quickselect": ["quickselect@2.0.0", "", {}, "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
"rbush": ["rbush@3.0.1", "", { "dependencies": { "quickselect": "^2.0.0" } }, "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
@@ -2694,10 +2679,6 @@
"@testing-library/react-native/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
"@turf/bbox/@turf/helpers": ["@turf/helpers@7.3.4", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g=="],
"@turf/bbox/@turf/meta": ["@turf/meta@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
@@ -2790,8 +2771,6 @@
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"geojson-rbush/@types/geojson": ["@types/geojson@7946.0.8", "", {}, "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="],
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],

View File

@@ -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();
}
}
}}

View 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;

View 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;

View 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,
},
});

View 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,
},
});

View File

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

View File

@@ -0,0 +1,256 @@
import { MainColor } from "@/constants/color-palet";
import {
DEFAULT_COUNTRY,
searchCountries,
type CountryData,
} from "@/constants/countries";
import { useState } from "react";
import {
Modal,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
interface PhoneInputProps {
value: string;
onChangePhoneNumber: (phone: string) => void;
selectedCountry?: CountryData;
onChangeCountry: (country: CountryData) => void;
placeholder?: string;
disabled?: boolean;
}
export default function PhoneInputCustom({
value,
onChangePhoneNumber,
selectedCountry = DEFAULT_COUNTRY,
onChangeCountry,
placeholder = "Masukkan nomor",
disabled = false,
}: PhoneInputProps) {
const [countryPickerVisible, setCountryPickerVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const filteredCountries = searchCountries(searchQuery);
const handleSelectCountry = (country: CountryData) => {
onChangeCountry(country);
setCountryPickerVisible(false);
setSearchQuery("");
};
const handlePhoneChange = (text: string) => {
// Only allow numbers and spaces
const cleaned = text.replace(/[^\d\s]/g, "");
onChangePhoneNumber(cleaned);
};
return (
<>
{/* Phone Input Field */}
<View style={styles.container}>
<TouchableOpacity
style={styles.countryPickerButton}
onPress={() => setCountryPickerVisible(true)}
disabled={disabled}
activeOpacity={0.7}
>
<Text style={styles.countryCodeText}>+{selectedCountry.callingCode}</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TextInput
style={[styles.phoneInput, disabled && styles.disabledInput]}
placeholder={placeholder}
placeholderTextColor={MainColor.placeholder}
value={value}
onChangeText={handlePhoneChange}
keyboardType="phone-pad"
autoComplete="tel"
importantForAutofill="yes"
editable={!disabled}
/>
</View>
{/* Country Picker Modal */}
<Modal
visible={countryPickerVisible}
transparent
animationType="slide"
onRequestClose={() => setCountryPickerVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Pilih Negara</Text>
<TouchableOpacity onPress={() => setCountryPickerVisible(false)}>
<Text style={styles.modalClose}></Text>
</TouchableOpacity>
</View>
<TextInput
style={styles.searchInput}
placeholder="Cari negara atau kode..."
placeholderTextColor={MainColor.placeholder}
value={searchQuery}
onChangeText={setSearchQuery}
autoFocus
/>
<ScrollView style={styles.countryList}>
{filteredCountries.map((country) => (
<TouchableOpacity
key={country.code}
style={[
styles.countryItem,
selectedCountry.code === country.code &&
styles.countryItemSelected,
]}
onPress={() => handleSelectCountry(country)}
activeOpacity={0.7}
>
<View style={styles.countryInfo}>
<Text style={styles.countryName}>{country.name}</Text>
<Text style={styles.countryCode}>+{country.callingCode}</Text>
</View>
{selectedCountry.code === country.code && (
<Text style={styles.checkmark}></Text>
)}
</TouchableOpacity>
))}
</ScrollView>
</View>
</View>
</Modal>
</>
);
}
const styles = StyleSheet.create({
// Container
container: {
flexDirection: "row",
backgroundColor: MainColor.white,
borderRadius: 8,
borderWidth: 1,
borderColor: MainColor.white_gray,
marginBottom: 16,
overflow: "hidden",
},
// Country Picker Button
countryPickerButton: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: MainColor.text_input,
borderRightWidth: 1,
borderRightColor: MainColor.white_gray,
},
countryCodeText: {
fontSize: 16,
color: MainColor.black,
fontWeight: "600",
},
dropdownIcon: {
fontSize: 18,
color: MainColor.placeholder,
marginLeft: 4,
},
// Divider
divider: {
width: 1,
backgroundColor: MainColor.white_gray,
},
// Phone Input
phoneInput: {
flex: 1,
paddingVertical: 14,
paddingHorizontal: 12,
fontSize: 16,
color: MainColor.black,
},
disabledInput: {
backgroundColor: MainColor.text_input,
color: MainColor.placeholder,
},
// Modal
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
},
modalContent: {
backgroundColor: MainColor.white,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: "80%",
paddingBottom: 34,
},
modalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 20,
borderBottomWidth: 1,
borderBottomColor: MainColor.white_gray,
},
modalTitle: {
fontSize: 18,
fontWeight: "bold",
color: MainColor.black,
},
modalClose: {
fontSize: 24,
color: MainColor.placeholder,
padding: 5,
},
// Search Input
searchInput: {
backgroundColor: MainColor.text_input,
margin: 16,
padding: 12,
borderRadius: 8,
fontSize: 16,
color: MainColor.black,
},
// Country List
countryList: {
paddingHorizontal: 16,
},
countryItem: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 12,
borderBottomWidth: 1,
borderBottomColor: MainColor.white_gray,
},
countryItemSelected: {
backgroundColor: MainColor.soft_darkblue + "15",
},
countryInfo: {
flex: 1,
},
countryName: {
fontSize: 16,
color: MainColor.black,
fontWeight: "500",
},
countryCode: {
fontSize: 14,
color: MainColor.placeholder,
marginTop: 2,
},
checkmark: {
fontSize: 20,
color: MainColor.green,
fontWeight: "bold",
},
});

View File

@@ -0,0 +1,254 @@
// @/components/AndroidWrapper.tsx
// Android Wrapper - Based on NewWrapper_V2 (with keyboard handling for Android)
import { MainColor } from "@/constants/color-palet";
import { OS_HEIGHT } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import {
ImageBackground,
Keyboard,
KeyboardAvoidingView,
ScrollView,
FlatList,
TouchableWithoutFeedback,
View,
StyleProp,
ViewStyle,
} from "react-native";
import {
NativeSafeAreaViewProps,
SafeAreaView,
} from "react-native-safe-area-context";
import type { ScrollViewProps, FlatListProps } from "react-native";
import { useKeyboardForm, cloneChildrenWithFocusHandler } from "@/hooks/useKeyboardForm";
// --- Base Props ---
interface BaseProps {
withBackground?: boolean;
headerComponent?: React.ReactNode;
footerComponent?: React.ReactNode;
floatingButton?: React.ReactNode;
hideFooter?: boolean;
edgesFooter?: NativeSafeAreaViewProps["edges"];
style?: StyleProp<ViewStyle>;
refreshControl?: ScrollViewProps["refreshControl"];
/**
* Enable keyboard handling with auto-scroll (Android only)
* @default false
*/
enableKeyboardHandling?: boolean;
/**
* Scroll offset when keyboard appears (Android only)
* @default 100
*/
keyboardScrollOffset?: number;
/**
* Extra padding bottom for content to avoid navigation bar (Android only)
* @default 80
*/
contentPaddingBottom?: number;
/**
* Padding untuk content container (Android only)
* Set to 0 untuk tidak ada padding, atau custom value sesuai kebutuhan
* @default 16
*/
contentPadding?: number;
/**
* Disable flexGrow: 1 in contentContainerStyle
* Use this for screens with very large headers to fix scroll issues
* @default false
*/
disableFlexGrow?: boolean;
}
interface StaticModeProps extends BaseProps {
children: React.ReactNode;
listData?: never;
renderItem?: never;
}
interface ListModeProps extends BaseProps {
children?: never;
listData?: any[];
renderItem?: FlatListProps<any>["renderItem"];
onEndReached?: () => void;
ListHeaderComponent?: React.ReactElement | null;
ListFooterComponent?: React.ReactElement | null;
ListEmptyComponent?: React.ReactElement | null;
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
type AndroidWrapperProps = StaticModeProps | ListModeProps;
export function AndroidWrapper(props: AndroidWrapperProps) {
const {
withBackground = false,
headerComponent,
footerComponent,
floatingButton,
hideFooter = false,
edgesFooter = [],
style,
refreshControl,
enableKeyboardHandling = false,
keyboardScrollOffset,
contentPaddingBottom,
contentPadding,
disableFlexGrow = false,
} = props;
// Default values (should be set by OS_Wrapper, but fallback for direct usage)
const finalKeyboardScrollOffset = keyboardScrollOffset ?? 100;
const finalContentPaddingBottom = contentPaddingBottom ?? 250;
const finalContentPadding = contentPadding ?? 0;
const assetBackground = require("../../assets/images/main-background.png");
// Use keyboard hook if enabled
const keyboardForm = enableKeyboardHandling
? useKeyboardForm(finalKeyboardScrollOffset)
: null;
const renderContainer = (content: React.ReactNode) => {
if (withBackground) {
return (
<ImageBackground
source={assetBackground}
resizeMode="cover"
style={GStyles.imageBackground}
>
<View style={[GStyles.containerWithBackground, style]}>
{content}
</View>
</ImageBackground>
);
}
return <View style={[GStyles.container, style]}>{content}</View>;
};
// 🔹 Mode Dinamis (FlatList)
if ("listData" in props) {
const listProps = props as ListModeProps;
return (
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<FlatList
style={{ flex: 1 }}
data={listProps.listData}
renderItem={listProps.renderItem}
keyExtractor={
listProps.keyExtractor ||
((item, index) => `${String(item.id)}-${index}`)
}
refreshControl={refreshControl}
onEndReached={listProps.onEndReached}
onEndReachedThreshold={0.5}
ListHeaderComponent={listProps.ListHeaderComponent}
ListFooterComponent={listProps.ListFooterComponent}
ListEmptyComponent={listProps.ListEmptyComponent}
contentContainerStyle={{
flexGrow: disableFlexGrow ? 0 : 1,
paddingBottom:
(footerComponent && !hideFooter ? OS_HEIGHT : 0) +
finalContentPaddingBottom,
padding: finalContentPadding,
}}
keyboardShouldPersistTaps="handled"
removeClippedSubviews={false}
stickyHeaderIndices={[]}
nestedScrollEnabled={true}
/>
{/* Footer - Fixed di bawah dengan width 100% */}
{footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue, width: "100%" }}
>
<View style={{ width: "100%" }}>{footerComponent}</View>
</SafeAreaView>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
/>
)}
{floatingButton && (
<View style={GStyles.floatingContainer}>{floatingButton}</View>
)}
</View>
);
}
// 🔹 Mode Statis (ScrollView)
const staticProps = props as StaticModeProps;
// Inject focus handler jika keyboard handling enabled
const childrenWithFocus = enableKeyboardHandling && keyboardForm
? cloneChildrenWithFocusHandler(staticProps.children, keyboardForm.handleInputFocus)
: staticProps.children;
return (
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<ScrollView
ref={keyboardForm?.scrollViewRef}
onScroll={keyboardForm?.handleScroll}
scrollEventThrottle={16}
refreshControl={refreshControl}
style={{ flex: 1 }}
contentContainerStyle={{
flexGrow: disableFlexGrow ? 0 : 1,
paddingBottom:
(footerComponent && !hideFooter ? OS_HEIGHT : 0) +
finalContentPaddingBottom,
padding: finalContentPadding,
}}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{renderContainer(childrenWithFocus)}
</TouchableWithoutFeedback>
</ScrollView>
{/* Footer - Fixed di bawah dengan width 100% */}
{footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{
backgroundColor: MainColor.darkblue,
width: "100%",
position: "absolute",
bottom: 0,
left: 0,
right: 0,
}}
>
<View style={{ width: "100%" }}>{footerComponent}</View>
</SafeAreaView>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
/>
)}
{floatingButton && (
<View style={GStyles.floatingContainer}>{floatingButton}</View>
)}
</View>
);
}
export default AndroidWrapper;

View File

@@ -0,0 +1,114 @@
import { MainColor } from "@/constants/color-palet";
import { Platform, StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { BackButton } from "..";
type Props = {
title: string;
right?: React.ReactNode;
showBack?: boolean;
onPressLeft?: () => void;
left?: React.ReactNode;
};
export default function AppHeader({
title,
right,
showBack = true,
onPressLeft,
left,
}: Props) {
const insets = useSafeAreaInsets();
// iOS 16+ detection (Dynamic Island) - insets.top > 47 indicates Dynamic Island
const isIOS26Plus =
Platform.OS === "ios" && insets.top > 47;
// Dynamic padding berdasarkan platform dan iOS version
const paddingTop =
Platform.OS === "ios"
? isIOS26Plus
? insets.top - 10
: insets.top
: 40;
const paddingBottom = Platform.OS === "ios" ? 8 : 13;
return (
<View
style={[
{
backgroundColor: MainColor.darkblue,
paddingTop,
paddingBottom,
},
]}
pointerEvents="box-none"
>
{/* Header Container dengan absolute positioning untuk title center */}
<View style={styles.headerApp} pointerEvents="box-none">
{/* Left Section - Absolute Left */}
<View style={styles.headerLeft}>
{showBack ? (
<BackButton onPress={onPressLeft} />
) : left ? (
left
) : (
<View style={styles.placeholder} />
)}
</View>
{/* Title - Absolute Center */}
<View style={styles.headerCenter}>
<Text
style={styles.headerTitle}
numberOfLines={1}
ellipsizeMode="tail"
>
{title ? title.charAt(0).toUpperCase() + title.slice(1) : ""}
</Text>
</View>
{/* Right Section - Absolute Right */}
<View style={styles.headerRight}>
{right}
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
headerApp: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
height: 44, // Fixed height untuk consistency
},
headerLeft: {
position: "absolute",
left: 16,
zIndex: 1,
},
headerCenter: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
headerRight: {
position: "absolute",
right: 16,
zIndex: 1,
},
placeholder: {
width: 40,
height: 40,
},
headerTitle: {
color: MainColor.yellow,
fontSize: 18,
fontWeight: "600",
textAlign: "center",
},
});

View File

@@ -0,0 +1,75 @@
// FormWrapper.tsx - Reusable wrapper untuk form dengan keyboard handling
import { MainColor } from "@/constants/color-palet";
import { Keyboard, KeyboardAvoidingView, Platform, ScrollView, TouchableWithoutFeedback, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { ReactNode } from "react";
import { useKeyboardForm } from "@/hooks/useKeyboardForm";
interface FormWrapperProps {
children: ReactNode;
footerComponent?: ReactNode;
/**
* Offset scroll saat keyboard muncul (default: 100)
*/
scrollOffset?: number;
/**
* Padding bottom untuk content (default: 100)
*/
contentPaddingBottom?: number;
/**
* Padding untuk content container (default: 16)
*/
contentPadding?: number;
}
export function FormWrapper({
children,
footerComponent,
scrollOffset = 100,
contentPaddingBottom = 100,
contentPadding = 16,
}: FormWrapperProps) {
const { scrollViewRef, handleScroll } = useKeyboardForm(scrollOffset);
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
>
<ScrollView
ref={scrollViewRef}
onScroll={handleScroll}
scrollEventThrottle={16}
style={{ flex: 1 }}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: contentPaddingBottom,
}}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={{ flex: 1, padding: contentPadding }}>
{children}
</View>
</TouchableWithoutFeedback>
</ScrollView>
{/* Footer - Fixed di bawah */}
{footerComponent && (
<SafeAreaView
edges={["bottom"]}
style={{
backgroundColor: MainColor.darkblue,
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
}}
>
{footerComponent}
</SafeAreaView>
)}
</KeyboardAvoidingView>
);
}

View File

@@ -0,0 +1,216 @@
// @/components/iOSWrapper.tsx
// iOS Wrapper - Based on NewWrapper (stable version for iOS)
import { MainColor } from "@/constants/color-palet";
import { OS_HEIGHT } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import {
ImageBackground,
Keyboard,
KeyboardAvoidingView,
ScrollView,
FlatList,
TouchableWithoutFeedback,
View,
StyleProp,
ViewStyle,
} from "react-native";
import {
NativeSafeAreaViewProps,
SafeAreaView,
} from "react-native-safe-area-context";
import type { ScrollViewProps, FlatListProps } from "react-native";
// --- Base Props ---
interface BaseProps {
withBackground?: boolean;
headerComponent?: React.ReactNode;
footerComponent?: React.ReactNode;
floatingButton?: React.ReactNode;
hideFooter?: boolean;
edgesFooter?: NativeSafeAreaViewProps["edges"];
style?: StyleProp<ViewStyle>;
refreshControl?: ScrollViewProps["refreshControl"];
}
interface StaticModeProps extends BaseProps {
children: React.ReactNode;
listData?: never;
renderItem?: never;
}
interface ListModeProps extends BaseProps {
children?: never;
listData?: any[];
renderItem?: FlatListProps<any>["renderItem"];
onEndReached?: () => void;
ListHeaderComponent?: React.ReactElement | null;
ListFooterComponent?: React.ReactElement | null;
ListEmptyComponent?: React.ReactElement | null;
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
type iOSWrapperProps = StaticModeProps | ListModeProps;
const iOSWrapper = (props: iOSWrapperProps) => {
const {
withBackground = false,
headerComponent,
footerComponent,
floatingButton,
hideFooter = false,
edgesFooter = [],
style,
refreshControl,
} = props;
const assetBackground = require("../../assets/images/main-background.png");
const renderContainer = (content: React.ReactNode) => {
if (withBackground) {
return (
<ImageBackground
source={assetBackground}
resizeMode="cover"
style={GStyles.imageBackground}
>
<View style={[GStyles.containerWithBackground, style]}>
{content}
</View>
</ImageBackground>
);
}
return <View style={[GStyles.container, style]}>{content}</View>;
};
// 🔹 Mode Dinamis (FlatList)
if ("listData" in props) {
const listProps = props as ListModeProps;
return (
<KeyboardAvoidingView
behavior="padding"
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
>
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<View style={[GStyles.container, style, { flex: 1 }]}>
<FlatList
data={listProps.listData}
renderItem={listProps.renderItem}
keyExtractor={
listProps.keyExtractor ||
((item, index) => {
if (item.id == null) {
console.warn("Item tanpa 'id':", item);
return `fallback-${index}-${JSON.stringify(item)}`;
}
return `${String(item.id)}-${index}`;
})
}
refreshControl={refreshControl}
onEndReached={listProps.onEndReached}
onEndReachedThreshold={0.5}
ListHeaderComponent={listProps.ListHeaderComponent}
ListFooterComponent={listProps.ListFooterComponent}
ListEmptyComponent={listProps.ListEmptyComponent}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0
}}
keyboardShouldPersistTaps="handled"
/>
</View>
{/* Footer - tetap di bawah dengan position absolute */}
{footerComponent && !hideFooter && (
<View style={styles.footerContainer}>
<SafeAreaView
edges={edgesFooter}
style={{ backgroundColor: MainColor.darkblue }}
>
{footerComponent}
</SafeAreaView>
</View>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
/>
)}
{floatingButton && (
<View style={GStyles.floatingContainer}>{floatingButton}</View>
)}
</KeyboardAvoidingView>
);
}
// 🔹 Mode Statis (ScrollView)
const staticProps = props as StaticModeProps;
return (
<KeyboardAvoidingView
behavior="padding"
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
>
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<View style={{ flex: 1 }}>
<ScrollView
contentContainerStyle={{
flexGrow: 1,
paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0
}}
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{renderContainer(staticProps.children)}
</TouchableWithoutFeedback>
</ScrollView>
</View>
{/* Footer - tetap di bawah dengan position absolute */}
{footerComponent && !hideFooter && (
<View style={styles.footerContainer}>
<SafeAreaView
edges={edgesFooter}
style={{ backgroundColor: MainColor.darkblue }}
>
{footerComponent}
</SafeAreaView>
</View>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
/>
)}
{floatingButton && (
<View style={GStyles.floatingContainer}>{floatingButton}</View>
)}
</KeyboardAvoidingView>
);
};
// Styles untuk footer dengan position absolute
const styles = {
footerContainer: {
position: "absolute" as const,
bottom: 0,
left: 0,
right: 0,
backgroundColor: MainColor.darkblue,
},
};
export default iOSWrapper;

View File

@@ -19,6 +19,7 @@ import {
SafeAreaView,
} from "react-native-safe-area-context";
import type { ScrollViewProps, FlatListProps } from "react-native";
import Spacing from "./Spacing";
// --- ✅ Tambahkan refreshControl ke BaseProps ---
interface BaseProps {
@@ -83,7 +84,7 @@ const NewWrapper = (props: NewWrapperProps) => {
return <View style={[GStyles.container, style]}>{content}</View>;
};
// 🔹 Mode Dinamis
// 🔹 Mode Dinamis (FlatList)
if ("listData" in props) {
const listProps = props as ListModeProps;
@@ -95,7 +96,7 @@ const NewWrapper = (props: NewWrapperProps) => {
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<View style={[GStyles.container, style]}>
<View style={[GStyles.container, style, { flex: 1 }]}>
<FlatList
data={listProps.listData}
renderItem={listProps.renderItem}
@@ -107,30 +108,36 @@ const NewWrapper = (props: NewWrapperProps) => {
return `fallback-${index}-${JSON.stringify(item)}`;
}
// Gabungkan ID dengan indeks untuk mencegah duplikasi
return `${String(item.id)}-${index}`;
})
}
refreshControl={refreshControl} // ✅ dari BaseProps
refreshControl={refreshControl}
onEndReached={listProps.onEndReached}
onEndReachedThreshold={0.5}
ListHeaderComponent={listProps.ListHeaderComponent}
ListFooterComponent={listProps.ListFooterComponent}
ListEmptyComponent={listProps.ListEmptyComponent}
contentContainerStyle={{ flexGrow: 1 }}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0
}}
keyboardShouldPersistTaps="handled"
/>
</View>
{footerComponent ? (
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
>
{footerComponent}
</SafeAreaView>
) : hideFooter ? null : (
{/* Footer - tetap di bawah dengan position absolute */}
{footerComponent && !hideFooter && (
<View style={styles.footerContainer}>
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
>
{footerComponent}
</SafeAreaView>
</View>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
@@ -144,7 +151,7 @@ const NewWrapper = (props: NewWrapperProps) => {
);
}
// 🔹 Mode Statis
// 🔹 Mode Statis (ScrollView)
const staticProps = props as StaticModeProps;
return (
@@ -156,24 +163,34 @@ const NewWrapper = (props: NewWrapperProps) => {
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl} // ✅ sekarang valid
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{renderContainer(staticProps.children)}
</TouchableWithoutFeedback>
</ScrollView>
{footerComponent ? (
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
<View style={{ flex: 1 }}>
<ScrollView
contentContainerStyle={{
flexGrow: 1,
paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0
}}
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
>
{footerComponent}
</SafeAreaView>
) : hideFooter ? null : (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{renderContainer(staticProps.children)}
</TouchableWithoutFeedback>
</ScrollView>
</View>
{/* Footer - tetap di bawah dengan position absolute */}
{footerComponent && !hideFooter && (
<View style={styles.footerContainer}>
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
>
{footerComponent}
</SafeAreaView>
</View>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
@@ -187,4 +204,15 @@ const NewWrapper = (props: NewWrapperProps) => {
);
};
// Styles untuk footer dengan position absolute
const styles = {
footerContainer: {
position: "absolute" as const,
bottom: 0,
left: 0,
right: 0,
backgroundColor: MainColor.darkblue,
},
};
export default NewWrapper;

View File

@@ -0,0 +1,231 @@
// NewWrapper_V2.tsx - Wrapper baru dengan keyboard handling
import { MainColor } from "@/constants/color-palet";
import { OS_HEIGHT } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import {
ImageBackground,
Keyboard,
KeyboardAvoidingView,
Platform,
ScrollView,
FlatList,
TouchableWithoutFeedback,
View,
StyleProp,
ViewStyle,
} from "react-native";
import {
NativeSafeAreaViewProps,
SafeAreaView,
} from "react-native-safe-area-context";
import type { ScrollViewProps, FlatListProps } from "react-native";
import Spacing from "./Spacing";
import { useKeyboardForm } from "@/hooks/useKeyboardForm";
interface BaseProps {
withBackground?: boolean;
headerComponent?: React.ReactNode;
footerComponent?: React.ReactNode;
floatingButton?: React.ReactNode;
hideFooter?: boolean;
edgesFooter?: NativeSafeAreaViewProps["edges"];
style?: StyleProp<ViewStyle>;
refreshControl?: ScrollViewProps["refreshControl"];
/**
* Enable keyboard handling with auto-scroll
* @default false
*/
enableKeyboardHandling?: boolean;
/**
* Scroll offset when keyboard appears (default: 100)
*/
keyboardScrollOffset?: number;
/**
* Extra padding bottom for content to avoid navigation bar (default: 80)
*/
contentPaddingBottom?: number;
/**
* Padding untuk content container (default: 16)
* Set to 0 untuk tidak ada padding, atau custom value sesuai kebutuhan
*/
contentPadding?: number;
}
interface StaticModeProps extends BaseProps {
children: React.ReactNode;
listData?: never;
renderItem?: never;
}
interface ListModeProps extends BaseProps {
children?: never;
listData?: any[];
renderItem?: FlatListProps<any>["renderItem"];
onEndReached?: () => void;
ListHeaderComponent?: React.ReactElement | null;
ListFooterComponent?: React.ReactElement | null;
ListEmptyComponent?: React.ReactElement | null;
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
type NewWrapper_V2_Props = StaticModeProps | ListModeProps;
export function NewWrapper_V2(props: NewWrapper_V2_Props) {
const {
withBackground = false,
headerComponent,
footerComponent,
floatingButton,
hideFooter = false,
edgesFooter = [],
style,
refreshControl,
enableKeyboardHandling = false,
keyboardScrollOffset = 100,
contentPaddingBottom = 80, // Default 80 untuk navigasi device
contentPadding = 16, // Default 16 untuk padding konsisten
} = props;
const assetBackground = require("../../assets/images/main-background.png");
// Use keyboard hook if enabled
const keyboardForm = enableKeyboardHandling
? useKeyboardForm(keyboardScrollOffset)
: null;
const renderContainer = (content: React.ReactNode) => {
if (withBackground) {
return (
<ImageBackground
source={assetBackground}
resizeMode="cover"
style={GStyles.imageBackground}
>
<View style={[GStyles.containerWithBackground, style]}>
{content}
</View>
</ImageBackground>
);
}
return <View style={[GStyles.container, style]}>{content}</View>;
};
// 🔹 Mode Dinamis (FlatList)
if ("listData" in props) {
const listProps = props as ListModeProps;
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
>
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<FlatList
data={listProps.listData}
renderItem={listProps.renderItem}
keyExtractor={
listProps.keyExtractor ||
((item, index) => `${String(item.id)}-${index}`)
}
refreshControl={refreshControl}
onEndReached={listProps.onEndReached}
onEndReachedThreshold={0.5}
ListHeaderComponent={listProps.ListHeaderComponent}
ListFooterComponent={listProps.ListFooterComponent}
ListEmptyComponent={listProps.ListEmptyComponent}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: (footerComponent && !hideFooter ? OS_HEIGHT : 0) + contentPaddingBottom,
padding: contentPadding,
}}
keyboardShouldPersistTaps="handled"
/>
{/* Footer - Fixed di bawah dengan width 100% */}
{footerComponent && !hideFooter && (
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue, width: "100%" }}
>
<View style={{ width: "100%" }}>
{footerComponent}
</View>
</SafeAreaView>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
/>
)}
{floatingButton && (
<View style={GStyles.floatingContainer}>{floatingButton}</View>
)}
</KeyboardAvoidingView>
);
}
// 🔹 Mode Statis (ScrollView)
const staticProps = props as StaticModeProps;
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
>
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<ScrollView
ref={keyboardForm?.scrollViewRef}
style={{ flex: 1 }}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: (footerComponent && !hideFooter ? OS_HEIGHT : 0) + contentPaddingBottom,
padding: contentPadding,
}}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{renderContainer(staticProps.children)}
</TouchableWithoutFeedback>
</ScrollView>
{/* Footer - Fixed di bawah dengan width 100% */}
{footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{
backgroundColor: MainColor.darkblue,
width: "100%",
position: Platform.OS === "android" ? "absolute" : undefined,
bottom: Platform.OS === "android" ? 0 : undefined,
left: 0,
right: 0,
}}
>
<View style={{ width: "100%" }}>
{footerComponent}
</View>
</SafeAreaView>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
/>
)}
{floatingButton && (
<View style={GStyles.floatingContainer}>{floatingButton}</View>
)}
</KeyboardAvoidingView>
);
}

View File

@@ -0,0 +1,157 @@
// @/components/OS_Wrapper.tsx
// OS-Specific Wrapper - Automatically routes to iOSWrapper or AndroidWrapper
// iOS: Uses NewWrapper (stable for iOS)
// Android: Uses NewWrapper_V2 (with keyboard handling)
import { Platform } from "react-native";
import type { ScrollViewProps, FlatListProps } from "react-native";
import {
NativeSafeAreaViewProps,
} from "react-native-safe-area-context";
import type { StyleProp, ViewStyle } from "react-native";
import IOSWrapper from "./IOSWrapper";
import AndroidWrapper from "./AndroidWrapper";
// ========== Base Props ==========
interface BaseProps {
withBackground?: boolean;
headerComponent?: React.ReactNode;
footerComponent?: React.ReactNode;
floatingButton?: React.ReactNode;
hideFooter?: boolean;
edgesFooter?: NativeSafeAreaViewProps["edges"];
style?: StyleProp<ViewStyle>;
refreshControl?: ScrollViewProps["refreshControl"];
disableFlexGrow?: boolean;
}
// ========== Static Mode Props ==========
interface StaticModeProps extends BaseProps {
children: React.ReactNode;
listData?: never;
renderItem?: never;
}
// ========== List Mode Props ==========
interface ListModeProps extends BaseProps {
children?: never;
listData?: any[];
renderItem?: FlatListProps<any>["renderItem"];
onEndReached?: () => void;
ListHeaderComponent?: React.ReactElement | null;
ListFooterComponent?: React.ReactElement | null;
ListEmptyComponent?: React.ReactElement | null;
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
// ========== Keyboard Handling Props (Android only) ==========
interface KeyboardHandlingProps {
/**
* Enable keyboard handling with auto-scroll (Android only)
* iOS ignores this prop
* @default false
*/
enableKeyboardHandling?: boolean;
/**
* Scroll offset when keyboard appears (Android only)
* iOS ignores this prop
* @default 100
*/
keyboardScrollOffset?: number;
/**
* Extra padding bottom for content (Android only)
* iOS ignores this prop
* @default 80
*/
contentPaddingBottom?: number;
/**
* Padding untuk content container (Android only)
* iOS ignores this prop
* @default 16
*/
contentPadding?: number;
}
// ========== Final Props Types ==========
type OS_WrapperStaticProps = StaticModeProps & KeyboardHandlingProps;
type OS_WrapperListProps = ListModeProps & KeyboardHandlingProps;
type OS_WrapperProps = OS_WrapperStaticProps | OS_WrapperListProps;
/**
* OS_Wrapper - Automatically selects iOSWrapper or AndroidWrapper based on platform
*
* Features:
* - Auto platform detection
* - Optional keyboard handling for Android forms
* - Unified API for all use cases
*
* @example Static Mode (Simple Content)
* ```tsx
* <OS_Wrapper>
* <YourContent />
* </OS_Wrapper>
* ```
*
* @example List Mode (with pagination)
* ```tsx
* <OS_Wrapper
* listData={data}
* renderItem={({ item }) => <ItemCard item={item} />}
* ListEmptyComponent={<EmptyState />}
* onEndReached={loadMore}
* />
* ```
*
* @example Form Mode (with keyboard handling - Android only)
* ```tsx
* <OS_Wrapper
* enableKeyboardHandling
* keyboardScrollOffset={150}
* contentPaddingBottom={100}
* footerComponent={<SubmitButton />}
* >
* <FormContent />
* </OS_Wrapper>
* ```
*/
export function OS_Wrapper(props: OS_WrapperProps) {
const {
enableKeyboardHandling = false,
keyboardScrollOffset = 100,
contentPaddingBottom = 100,
contentPadding = 0,
disableFlexGrow = false,
...wrapperProps
} = props;
// iOS uses IOSWrapper (based on NewWrapper)
if (Platform.OS === "ios") {
// Keyboard handling props are ignored on iOS
return <IOSWrapper {...(wrapperProps as any)} disableFlexGrow={disableFlexGrow} />;
}
// Android uses AndroidWrapper (with keyboard handling support)
return (
<AndroidWrapper
{...(wrapperProps as any)}
enableKeyboardHandling={enableKeyboardHandling}
keyboardScrollOffset={keyboardScrollOffset}
contentPaddingBottom={contentPaddingBottom}
contentPadding={contentPadding}
disableFlexGrow={disableFlexGrow}
/>
);
}
// Re-export individual wrappers for direct usage if needed
export { default as IOSWrapper } from "./IOSWrapper";
export { default as AndroidWrapper } from "./AndroidWrapper";
// Legacy export untuk backward compatibility
export { IOSWrapper as iOSWrapper };
export default OS_Wrapper;

View File

@@ -49,7 +49,7 @@ const CustomSkeleton: React.FC<CustomSkeletonProps> = ({
right: 0,
height: 100,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 4,
borderRadius: 1,
}}
/>
</View>

View File

@@ -49,6 +49,8 @@ import MapCustom from "./Map/MapCustom";
import CenterCustom from "./Center/CenterCustom";
// Clickable
import ClickableCustom from "./Clickable/ClickableCustom";
// PhoneInput
import PhoneInputCustom from "./PhoneInput/PhoneInputCustom";
// Scroll
import ScrollableCustom from "./Scroll/ScrollCustom";
// ShareComponent
@@ -61,6 +63,11 @@ import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
import GridComponentView from "./_ShareComponent/GridSectionView";
import NewWrapper from "./_ShareComponent/NewWrapper";
import BasicWrapper from "./_ShareComponent/BasicWrapper";
import { FormWrapper } from "./_ShareComponent/FormWrapper";
import { NewWrapper_V2 } from "./_ShareComponent/NewWrapper_V2";
// OS-Specific Wrappers
import OS_Wrapper, { IOSWrapper, AndroidWrapper } from "./_ShareComponent/OS_Wrapper";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
// Loader
@@ -95,6 +102,8 @@ export {
CheckboxGroup,
// Clickable
ClickableCustom,
// PhoneInput
PhoneInputCustom,
// Container
CircleContainer,
// Divider
@@ -123,6 +132,12 @@ export {
Spacing,
NewWrapper,
BasicWrapper,
FormWrapper,
NewWrapper_V2,
// OS-Specific Wrappers
OS_Wrapper,
IOSWrapper,
AndroidWrapper,
// Stack
StackCustom,
TabBarBackground,

Some files were not shown because too many files have changed in this diff Show More