Compare commits

...

87 Commits

Author SHA1 Message Date
e7698618a2 Merge pull request 'server/14-apr-26' (#69) from server/14-apr-26 into staging
Reviewed-on: #69
2026-04-14 16:38:23 +08:00
a821aeb129 chore: remove unused test file
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 15:42:58 +08:00
0dd939b979 chore(release): 1.7.5 2026-04-14 15:34:52 +08:00
38239c52d6 fix: override MIME type for PDF uploads & fix build errors
- Fix PDF upload failing on Android due to wrong MIME type (image/pdf → application/pdf)
- Add safe JSON parsing for external storage responses with incorrect Content-Type headers
- Add detailed upload logging for debugging (file name, type, size, response status)
- Fix TypeScript error: missing prisma import in forum preview-report-posting route
- Fix ESLint warning: unescaped apostrophe in support-center page
- Add DEBUG_UPLOAD_FILE.md documentation for troubleshooting guide

Fixes upload issue where Android devices couldn't upload PDF files due to MIME type mismatch

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-14 15:17:45 +08:00
a71997b4ef Fix version 1.7.4
## no issue
2026-03-30 15:49:07 +08:00
0f584f8c72 chore(release): 1.7.4 2026-03-30 15:48:42 +08:00
445801941d fix 2026-03-27 17:59:59 +08:00
defafe694f chore(release): 1.7.3 2026-03-27 17:22:38 +08:00
cd3a9cc223 Fix server
### Issue:  process.listenerCount()
2026-03-13 16:44:25 +08:00
0eb31073b7 chore(release): 1.7.2 2026-03-13 16:43:35 +08:00
d33296d23b Fix validasi mobile
### No issue;
2026-03-11 14:12:07 +08:00
f7d05783c7 chore(release): 1.7.1 2026-03-11 13:56:09 +08:00
0f4abea990 Fix Bug & Clean Code
### No Issue"
2026-03-10 16:22:36 +08:00
a03c1fa575 Fix Docker file and Clean code
### No issue
2026-03-10 15:07:54 +08:00
fe457cd2d4 chore(release): 1.7.0 2026-03-10 15:05:46 +08:00
73cbf3640a feat: Tambahkan deep link handler untuk event confirmation
Deskripsi:
- Membuat route handler /event/[id]/confirmation untuk deep link mobile
- Menambahkan deteksi platform (iOS, Android, Web) dari user agent
- Memperbaiki Content-Type header untuk file .well-known (AASA & assetlinks)
- Menambahkan route ke public middleware agar bisa diakses tanpa auth

File yang diubah:
- src/app/event/[id]/confirmation/route.ts (baru)
- src/middleware.tsx (tambah public route)
- next.config.js (tambah headers untuk .well-known)

Testing:
- File .well-known accessible:  YES
- Content-Type header correct:  YES
- Deep link route works:  YES
- Platform detection works:  YES

### No Issue
2026-03-09 15:32:54 +08:00
dc6fa562cc Clean code
modified:   public/.well-known/assetlinks.json
modified:   src/lib/code-otp-sender.ts

### No issue
2026-03-06 16:35:34 +08:00
4fd7bb4a17 chore(release): 1.6.9 2026-03-06 16:29:50 +08:00
9199cc6971 Merge pull request 'fix-server' (#68) from fix-server/5-mar-26 into staging
Reviewed-on: #68
2026-03-05 16:44:21 +08:00
b2305a35a6 Fix WA Otp
### NO Issue
2026-03-05 16:38:31 +08:00
cbfd105134 chore(release): 1.6.8 2026-03-05 14:30:03 +08:00
3e6c94d77f Usulan Commit Message
fix: Implementasi retry mechanism dan error handling untuk database connections

Deskripsi:

Menambahkan withRetry wrapper pada berbagai API routes untuk menangani transient database errors dan meningkatkan reliabilitas koneksi

Memperbaiki error handling pada notification, authentication, dan user validation endpoints dengan response 503 untuk database connection errors

Update prisma.ts dengan konfigurasi logging yang lebih baik dan datasources configuration

Menambahkan validasi input parameters pada beberapa endpoints

Update dokumentasi QWEN.md dengan commit message format dan comment standards

Update .env.example dengan connection pool settings yang lebih lengkap

File yang diubah:

src/lib/prisma.ts — Konfigurasi Prisma client & logging

src/app/api/admin/notifikasi/count/route.tsx

src/app/api/auth/mobile-login/route.ts

src/app/api/mobile/notification/[id]/route.ts

src/app/api/user-validate/route.ts

Dan 27 file API routes lainnya (penerapan withRetry secara konsisten)

QWEN.md — Dokumentasi commit & comment standards

.env.example — Database connection pool configuration

### No Issue
2026-03-05 14:28:45 +08:00
74d90ea6aa Merge pull request 'fix-server/4-mar-26' (#67) from fix-server/4-mar-26 into staging
Reviewed-on: #67
2026-03-04 16:44:05 +08:00
a6c9182a01 Fix Server dengan penerapan Github build 2026-03-04 16:38:58 +08:00
453aa0a4ec chore(release): 1.6.7 2026-03-04 16:36:57 +08:00
fe37cce13e Fix publish.yml 2026-03-04 16:08:59 +08:00
ee05d0c71f Build Github 2026-03-04 15:18:01 +08:00
f8319b9ab5 Build with Github 2026-03-04 14:12:12 +08:00
edc9d532d4 Merge pull request 'bug-prisma 4' (#66) from bug-prisma/3-mar-26 into staging
Reviewed-on: #66
2026-03-03 16:47:49 +08:00
2c1d74973b Fix bug
modified:   next.config.js
        modified:   src/app/api/mobile/forum/[id]/preview-report-comment/route.ts

### No Issue
2026-03-03 16:42:39 +08:00
04c2e0d580 chore(release): 1.6.6 2026-03-03 16:41:45 +08:00
bipproduction
6dba07baac fix: prisma connection exhaustion & firebase lazy init
- prisma/schema.prisma: tambah binaryTargets debian & linux-musl untuk Docker
- src/lib/prisma.ts: pakai global singleton di dev & prod, hapus eager $connect()
- src/lib/firebase-admin.ts: lazy initialization agar tidak crash saat build time
- .env.example: lengkap dengan semua env variable + connection_limit & pool_timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 16:26:48 +08:00
b76c7a4b1c Merge pull request 'bug-prisma 3' (#65) from bug-prisma/3-mar-26 into staging
Reviewed-on: #65
2026-03-03 15:36:43 +08:00
240f6eb7c2 Fix console 2026-03-03 15:34:36 +08:00
f64ae42825 Fix Prisma
1 fix: error koneksi Prisma dengan retry mechanism
      2
      3 Perubahan:
      4 - src/lib/prisma.ts: Tambah retry (3x) dengan exponential backoff saat connect
      5 - src/lib/prisma-retry.ts: NEW - Utility wrapper untuk retry operations
      6 - src/app/api/user-validate/route.ts: Improve error logging dengan detail
      7 - src/middleware.tsx: Clean up commented code
      8
      9 Fitur:
     10 - Auto retry saat database connection gagal
     11 - Explicit () di production
     12 - Better error logging untuk debugging
     13 - Reusable retry wrapper (withRetry, withTimeout)
     14
     15 Testing:
     16 - Build berhasil 
     17 - Type checking passed 
     18
     19 Fixes: Error in PostgreSQL connection: Error { kind: Closed, cause: None }

### No Issue
2026-03-03 15:30:34 +08:00
df5d1aad48 chore(release): 1.6.5 2026-03-03 15:25:39 +08:00
82e69309a1 Merge pull request 'bug-prisma 2' (#64) from bug-prisma/3-mar-26 into staging
Reviewed-on: #64
2026-03-03 14:19:10 +08:00
a6588818b5 fix: error koneksi Prisma - DATABASE_URL tidak loaded di
production
- Tambah validasi DATABASE_URL di prisma.ts
- Tambah copy .env file di postbuild script

### No Issue
2026-03-03 14:15:15 +08:00
58e1afaa45 chore(release): 1.6.4 2026-03-03 14:14:00 +08:00
f65f9b7834 Merge pull request 'bug-prisma' (#63) from bug-prisma/3-mar-26 into staging
Reviewed-on: #63
2026-03-03 12:11:04 +08:00
250b7c5261 Fix Prisma 2026-03-03 12:03:30 +08:00
935e519662 chore(release): 1.6.3 2026-03-03 11:55:25 +08:00
36b9248ed7 Merge pull request 'fix-bug middle' (#62) from fix-bug/25-feb-26 into staging
Reviewed-on: #62
2026-02-25 15:35:20 +08:00
c6dbd152d5 Fix middleware
### NO Issue
2026-02-25 15:34:22 +08:00
714cf5cd5a chore(release): 1.6.2 2026-02-25 15:22:40 +08:00
a2c5f053da Merge pull request 'fix-bug/25-feb-26' (#61) from fix-bug/25-feb-26 into staging
Reviewed-on: #61
2026-02-25 14:34:14 +08:00
a68343599d Fix type env
Fix:
- modified:   types/env.d.ts

### No Issue
2026-02-25 14:25:13 +08:00
419b87fc92 chore(release): 1.6.1 2026-02-25 12:00:20 +08:00
bedc0cc88b Merge pull request 'Delete termsOfServiceAccepted on register' (#60) from mobile-api/24-feb-26 into staging
Reviewed-on: #60
2026-02-25 10:46:17 +08:00
0271c87ba9 Delete termsOfServiceAccepted on register
### No issue
2026-02-24 18:04:05 +08:00
a9fbd544e5 Merge pull request 'mobile-api/24-feb-26' (#59) from mobile-api/24-feb-26 into staging
Reviewed-on: #59
2026-02-24 07:42:44 +08:00
5551f30721 Fix API and clear code
modified:   src/app/api/auth/register/route.ts
 modified:   src/app_modules/auth/login/view.tsx

### No Issue
2026-02-24 07:38:44 +08:00
00d36454d1 chore(release): 1.6.0 2026-02-24 07:33:28 +08:00
a762fbe9b1 Fix Api Mobile
API – Admin Forum
- src/app/api/mobile/admin/forum/route.ts
- src/app/api/mobile/admin/forum/[id]/comment/route.ts
- src/app/api/mobile/admin/forum/[id]/report-posting/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-20 16:47:28 +08:00
a98ab18423 Fix API Mobile
API – Admin Forum & Investment
- src/app/api/mobile/admin/forum/route.ts
- src/app/api/mobile/admin/investment/route.ts
- src/app/api/mobile/admin/investment/[id]/investor/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-19 16:44:17 +08:00
9afd741d4f Merge pull request 'Fix Admin API Mobile' (#58) from mobile-api/18-feb-26 into staging
Reviewed-on: #58
2026-02-18 17:33:11 +08:00
1c227a2850 Fix Admin API Mobile
API – Admin Donation
- src/app/api/mobile/admin/donation/[id]/disbursement/route.ts
- src/app/api/mobile/admin/donation/[id]/donatur/route.ts
- src/app/api/mobile/admin/donation/route.ts

API – Master Data (Admin)
- src/app/api/mobile/admin/master/donation/route.ts
- src/app/api/mobile/admin/master/type-of-event/route.ts

API – Admin Voting
- src/app/api/mobile/admin/voting/route.ts

Docs
- PROMPT-AI.md
- QWEN.md

Deleted
- CHANGELOG_BRANCH.md

### No Issue
2026-02-18 17:22:54 +08:00
817919f8f7 Merge pull request '### Fitur: Penambahan Pagination pada Endpoint Admin Mobile' (#57) from mobile-api/14-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/57
2026-02-14 16:25:39 +08:00
5bdb998d2e ### Fitur: Penambahan Pagination pada Endpoint Admin Mobile
#### Deskripsi Umum
Telah dilakukan penambahan fitur pagination pada beberapa endpoint admin mobile untuk meningkatkan kinerja dan pengalaman pengguna saat mengakses data dalam jumlah besar.

#### File yang Diubah

1. **src/app/api/mobile/admin/job/route.ts**
   - Ditambahkan parameter  dari
   - Diterapkan logika pagination dengan  (default 10) dan
   - Query  telah dimodifikasi untuk mendukung pagination

2. **src/app/api/mobile/admin/event/route.ts**
   - Diperbaiki definisi variabel  untuk memastikan tipe data yang konsisten
   - Ditambahkan default value 1 untuk parameter
   - Perhitungan  disesuaikan agar lebih efisien

3. **src/app/api/mobile/admin/event/[id]/participants/route.ts**
   - Ditambahkan parameter  dari
   - Diterapkan logika pagination dengan  (default 10) dan
   - Query  telah dimodifikasi untuk mendukung pagination

#### Tujuan Perubahan
- Meningkatkan kinerja aplikasi saat mengambil data dalam jumlah besar
- Memungkinkan pengguna untuk mengakses data secara bertahap melalui halaman-halaman
- Mengurangi beban server saat mengambil data dalam jumlah besar
- Memberikan pengalaman pengguna yang lebih baik saat mengakses data admin

#### Cara Penggunaan
Untuk menggunakan fitur pagination, cukup tambahkan parameter  pada query string saat melakukan permintaan ke endpoint yang telah dimodifikasi. Contoh:

Default jumlah data per halaman adalah 10 item.

### No Issue
2026-02-14 15:36:09 +08:00
90031e23ef Merge pull request 'Fix Api Mobile' (#56) from mobile-api/13-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/56
2026-02-13 17:41:31 +08:00
b585aa3024 Fix Api Mobile
API – Admin Master Data
- src/app/api/mobile/admin/master/bank/route.ts
- src/app/api/mobile/admin/master/business-field/route.ts
- src/app/api/mobile/admin/master/business-field/[id]/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-13 17:40:25 +08:00
596ebd2ff4 Merge pull request 'mobile-api/12-feb-26' (#55) from mobile-api/12-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/55
2026-02-12 17:49:00 +08:00
a8f9d2ac0d Fix API Mobile Admin
API – Admin User (Mobile)
- src/app/api/mobile/admin/user/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-12 17:42:06 +08:00
d43f3762a3 Fixed Bug Server
## Summary
This branch contains several bug fixes and performance improvements, primarily focusing on:
- Database connection management
- MQTT client stability
- Logging optimization
- API enhancements

## Detailed Changes

### Fixed Issues
1. **Database Connection Management**
   - Removed  from user-validate API route to prevent connection pool exhaustion
   - Added proper connection handling in global Prisma setup
   - Reduced logging verbosity in production environments

2. **MQTT Client Improvements**
   - Enhanced MQTT client initialization with proper error handling
   - Added reconnection logic with configurable intervals
   - Implemented cleanup functions to prevent memory leaks
   - Added separate initialization logic for server and client-side code

3. **Logging Optimization**
   - Removed excessive logging in middleware that was causing high CPU usage
   - Configured appropriate log levels for development and production

4. **Component Stability**
   - Added safety checks in text editor component to prevent MQTT operations on the server side
   - Improved MQTT publishing logic with client availability checks

### New Files
-  - Utility functions for safe database operations

### Modified Files
1.
   - Removed problematic  call

2.
   - Configured different logging levels for dev/prod
   - Removed process listeners that were causing disconnections
   - Exported prisma instance separately

3.
   - Removed excessive logging statements

4.
   - Enhanced initialization with error handling
   - Added reconnection and timeout configurations

5.
   - Added proper cleanup functions
   - Improved connection handling

6.
   - Added MQTT client availability checks
   - Prevented server-side MQTT operations

### Performance Improvements
- Reduced database connection overhead
- Optimized MQTT connection handling
- Eliminated unnecessary logging in production
- Better memory management with proper cleanup functions

### No Issue
2026-02-12 16:29:03 +08:00
aa700523ca Merge pull request 'feat: Implementasi pagination pada endpoint mobile donation' (#54) from mobile-api/10-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/54
2026-02-10 17:36:02 +08:00
e89886e1db feat: Implementasi pagination pada endpoint mobile donation
4
      5 - Menambahkan pagination pada endpoint GET /api/mobile/donation
      6 - Menambahkan pagination pada endpoint GET /api/mobile/donation/[id]/news
      7 - Menambahkan pagination pada endpoint GET /api/mobile/donation/[id]/donatur
      8 - Memperbaiki validasi payload pada endpoint POST /api/mobile/auth/device-tokens
      9 - Menangani struktur payload yang bersarang dan langsung pada device token endpoint
     10 - Menambahkan informasi pagination ke dalam respons API

### NO Issue
2026-02-10 17:31:39 +08:00
236ab4d4a4 Merge pull request 'Mobile API' (#53) from mobile-api/9-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/53
2026-02-09 17:38:52 +08:00
934d6a3ef1 feat: update mobile donation API and related dependencies
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-09 17:34:49 +08:00
a7694bd7d5 feat: Tambahkan pagination pada API mobile investasi
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Deskripsi:

- Tambahkan pagination pada endpoint investor/route.ts

- Tambahkan pagination pada endpoint news/route.ts untuk kategori all-news

- Gunakan konstanta PAGINATION_DEFAULT_TAKE untuk jumlah data per halaman

- Tambahkan metadata pagination (currentPage, totalData, totalPage, dataPerPage)

Fixes #issue-number
2026-02-09 15:08:33 +08:00
eaa7692359 Merge pull request 'Fix API mobile Investment' (#52) from mobile-api/6-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/52
2026-02-06 17:39:36 +08:00
3b0ea3d847 chore(release): 1.5.40 2026-02-06 17:38:19 +08:00
097758a431 Fix API mobile Investment
API – Investment (Mobile)
- src/app/api/mobile/investment/route.ts
- src/app/api/mobile/investment/[id]/[status]/route.ts
- src/app/api/mobile/investment/[id]/invoice/route.ts
- src/app/api/mobile/investment/[id]/document/route.ts

Docs
- PROMPT-AI.md

### No issue
2026-02-06 17:37:47 +08:00
d51ce346e6 Merge pull request 'Fix API mobile' (#51) from mobile-api/5-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/51
2026-02-05 17:36:40 +08:00
6f5849aa29 Fix API mobile
API – Voting (User)
- src/app/api/mobile/voting/route.ts
- src/app/api/mobile/voting/[id]/[status]/route.ts
- src/app/api/mobile/voting/[id]/contribution/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-05 17:35:49 +08:00
91f4bb6c9e Merge pull request 'mobile-api/4-jan-26' (#50) from mobile-api/4-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/50
2026-02-05 10:11:03 +08:00
1fe0001994 Merge pull request 'Fix API Job untuk loaddata:' (#49) from mobile-api/2-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/49
2026-02-02 17:11:32 +08:00
b82a283731 Merge pull request 'mobile-api for load data' (#48) from mobile-api/30-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/48
2026-01-30 17:20:09 +08:00
6d7d0fd07e Merge pull request 'mobile-notification done' (#46) from mobile-notification/27-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/46
2026-01-27 17:00:26 +08:00
bc80bb3441 Merge pull request 'Notification Donasi & EULA on login' (#45) from mobile-notification/23-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/45
2026-01-23 17:06:56 +08:00
8ab94b9c86 Merge pull request 'mobile-notification invesment' (#44) from mobile-notification/21-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/44
2026-01-21 15:41:18 +08:00
6e37b18e42 Merge pull request 'mobile-notification report comment' (#43) from mobile-notification/19-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/43
2026-01-19 17:54:21 +08:00
a6db03d0b4 Merge pull request 'mobile-notification event dan voting' (#42) from mobile-notification/15-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/42
2026-01-15 17:41:58 +08:00
c550a4e922 Merge pull request 'mobile-notification try to push to apple and android preview' (#41) from mobile-notification/12-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/41
2026-01-15 17:41:13 +08:00
2431a3fa3e Merge pull request 'Mobile notification & EULA route' (#40) from mobile-notification/9-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/40
2026-01-09 17:47:40 +08:00
1ed0da8c7d Merge pull request 'Fix API mobile notifikasi untuk job' (#39) from mobile-notification/7-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/39
2026-01-08 15:26:19 +08:00
e15a5d796d Merge pull request 'mobile notification' (#38) from mobile-notification/6-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/38
2026-01-06 17:53:14 +08:00
836ebfaef0 Merge pull request 'mobile notification API' (#37) from mobile-notification/5-jan-25 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/37
2026-01-05 14:06:58 +08:00
124 changed files with 2205 additions and 2336 deletions

56
.env.example Normal file
View File

@@ -0,0 +1,56 @@
# ==============================
# Database
# ==============================
# Connection pool settings untuk mencegah connection exhaustion:
# - connection_limit=10: Maksimal 10 koneksi per Prisma Client instance
# - pool_timeout=20: Timeout menunggu koneksi tersedia (detik)
# - connect_timeout=10: Timeout untuk membuat koneksi baru (detik)
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?connection_limit=10&pool_timeout=20&connect_timeout=10"
# ==============================
# Auth / Session
# ==============================
WIBU_PWD="your_wibu_password"
NEXT_PUBLIC_BASE_TOKEN_KEY="your_token_key"
NEXT_PUBLIC_BASE_SESSION_KEY="your_session_key"
# ==============================
# Payment Gateway (Midtrans)
# ==============================
Client_KEY="your_midtrans_client_key"
Server_KEY="your_midtrans_server_key"
# ==============================
# Maps
# ==============================
MAPBOX_TOKEN="your_mapbox_token"
# ==============================
# Realtime (WebSocket)
# ==============================
WS_APIKEY="your_ws_api_key"
NEXT_PUBLIC_WIBU_REALTIME_TOKEN="your_realtime_token"
# ==============================
# Email (Resend)
# ==============================
RESEND_APIKEY="your_resend_api_key"
# ==============================
# WhatsApp
# ==============================
WA_SERVER_TOKEN="your_wa_server_token"
# ==============================
# Firebase Admin (Push Notification)
# ==============================
FIREBASE_ADMIN_PROJECT_ID="your_firebase_project_id"
FIREBASE_ADMIN_CLIENT_EMAIL="your_firebase_client_email@project.iam.gserviceaccount.com"
# Private key: salin dari service account JSON, ganti newline dengan \n
FIREBASE_ADMIN_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n"
# ==============================
# App
# ==============================
NEXT_PUBLIC_API_URL="http://localhost:3000"
LOG_LEVEL="info"

72
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Publish Docker to GHCR
on:
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options:
- production
- staging
tag:
description: "Image tag (e.g. v1.0.0)"
required: true
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
name: Build & Push to GHCR (${{ github.event.inputs.environment }})
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
df -h
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ github.event.inputs.environment }}-${{ github.event.inputs.tag }}
type=raw,value=${{ github.event.inputs.environment }}-latest
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
no-cache: true

View File

@@ -2,6 +2,67 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [1.7.5](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.4...v1.7.5) (2026-04-14)
### Bug Fixes
* override MIME type for PDF uploads & fix build errors ([38239c5](https://wibugit.wibudev.com/wibu/hipmi/commit/38239c52d6d083d0c6ee6fde94f09e16fa7201dd))
## [1.7.4](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.3...v1.7.4) (2026-03-30)
## [1.7.3](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.2...v1.7.3) (2026-03-27)
## [1.7.2](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.1...v1.7.2) (2026-03-13)
## [1.7.1](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.0...v1.7.1) (2026-03-11)
## [1.7.0](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.9...v1.7.0) (2026-03-10)
### Features
* Tambahkan deep link handler untuk event confirmation ([73cbf36](https://wibugit.wibudev.com/wibu/hipmi/commit/73cbf3640ac795995e15448b24408b179d2a46d2))
## [1.6.9](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.8...v1.6.9) (2026-03-06)
## [1.6.8](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.7...v1.6.8) (2026-03-05)
## [1.6.7](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.6...v1.6.7) (2026-03-04)
## [1.6.6](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.5...v1.6.6) (2026-03-03)
### Bug Fixes
* prisma connection exhaustion & firebase lazy init ([6dba07b](https://wibugit.wibudev.com/wibu/hipmi/commit/6dba07baac6a3ef7d264c13e378a905002401e1b))
## [1.6.5](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.4...v1.6.5) (2026-03-03)
### Bug Fixes
* error koneksi Prisma - DATABASE_URL tidak loaded di ([a658881](https://wibugit.wibudev.com/wibu/hipmi/commit/a6588818b5d8018b3a634e0ae0846e309569d370))
## [1.6.4](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.3...v1.6.4) (2026-03-03)
## [1.6.3](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.2...v1.6.3) (2026-03-03)
## [1.6.2](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.1...v1.6.2) (2026-02-25)
## [1.6.1](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.0...v1.6.1) (2026-02-25)
## [1.6.0](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.40...v1.6.0) (2026-02-23)
### Features
* Implementasi pagination pada endpoint mobile donation ([e89886e](https://wibugit.wibudev.com/wibu/hipmi/commit/e89886e1dbc8cb4d95e6cc7c2787fb22a1dcaf56))
* Tambahkan pagination pada API mobile investasi ([a7694bd](https://wibugit.wibudev.com/wibu/hipmi/commit/a7694bd7d5d72b6499443faf99301faca730d3ed))
* update mobile donation API and related dependencies ([934d6a3](https://wibugit.wibudev.com/wibu/hipmi/commit/934d6a3ef1015367bee85779796df4f11c5e779c))
## [1.5.40](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.39...v1.5.40) (2026-02-06)
## [1.5.39](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.38...v1.5.39) (2026-01-30)
## [1.5.38](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.37...v1.5.38) (2026-01-27)

39
CHANGELOG_COMMIT.md Normal file
View File

@@ -0,0 +1,39 @@
## Catatan Perubahan untuk Commit
### Fitur: Penambahan Pagination pada Endpoint Admin Mobile
#### Deskripsi Umum
Telah dilakukan penambahan fitur pagination pada beberapa endpoint admin mobile untuk meningkatkan kinerja dan pengalaman pengguna saat mengakses data dalam jumlah besar.
#### File yang Diubah
1. **src/app/api/mobile/admin/job/route.ts**
- Ditambahkan parameter `page` dari `searchParams`
- Diterapkan logika pagination dengan `takeData` (default 10) dan `skipData`
- Query `prisma.job.findMany` telah dimodifikasi untuk mendukung pagination
2. **src/app/api/mobile/admin/event/route.ts**
- Diperbaiki definisi variabel `page` untuk memastikan tipe data yang konsisten
- Ditambahkan default value 1 untuk parameter `page`
- Perhitungan `skipData` disesuaikan agar lebih efisien
3. **src/app/api/mobile/admin/event/[id]/participants/route.ts**
- Ditambahkan parameter `page` dari `searchParams`
- Diterapkan logika pagination dengan `takeData` (default 10) dan `skipData`
- Query `prisma.event_Peserta.findMany` telah dimodifikasi untuk mendukung pagination
#### Tujuan Perubahan
- Meningkatkan kinerja aplikasi saat mengambil data dalam jumlah besar
- Memungkinkan pengguna untuk mengakses data secara bertahap melalui halaman-halaman
- Mengurangi beban server saat mengambil data dalam jumlah besar
- Memberikan pengalaman pengguna yang lebih baik saat mengakses data admin
#### Cara Penggunaan
Untuk menggunakan fitur pagination, cukup tambahkan parameter `page` pada query string saat melakukan permintaan ke endpoint yang telah dimodifikasi. Contoh:
```
GET /api/mobile/admin/job?page=2
GET /api/mobile/admin/event?page=3
GET /api/mobile/admin/event/{id}/participants?page=1
```
Default jumlah data per halaman adalah 10 item.

170
DEBUG_UPLOAD_FILE.md Normal file
View File

@@ -0,0 +1,170 @@
# Debug Guide: Upload File Android vs iOS
## 📱 Problem
- ✅ Upload **IMAGE** berhasil di iOS dan Android
- ❌ Upload **PDF** gagal di Android dengan error: `Status: 400 Bad Request`
## 🔍 Root Cause (DITEMUKAN!)
### **Masalah MIME Type PDF**
Dari log upload:
```
File details:
- Name: 154ce3b0-6fc0-4a39-9e09-3f9aa2b19300.pdf
- Type: image/pdf ← ❌ SALAH!
- Size: 26534 bytes
```
**Yang benar:**
-`image/pdf` (salah - tidak ada MIME type ini)
-`application/pdf` (benar - standard MIME type untuk PDF)
### **Kenapa Terjadi?**
Mobile app (Android) salah set MIME type saat mengirim file PDF. Kemungkinan:
1. File picker/set MIME type salah di mobile code
2. Android WebView auto-detect MIME type incorrectly
3. Mobile app hardcoded MIME type yang salah
## 🛠️ Solusi yang Sudah Diterapkan
### File: `src/app/api/mobile/file/route.ts`
**Fix #1: Safe JSON Parsing**
- ✅ Cek response sebagai text dulu, lalu parse JSON
- ✅ Handle Content-Type yang salah dari external storage
**Fix #2: MIME Type Override (LATEST)**
- ✅ Deteksi file PDF dari extension (.pdf)
- ✅ Override MIME type ke `application/pdf` jika salah
- ✅ Rebuild FormData dengan file yang sudah difix
**Code:**
```typescript
// Jika file PDF tapi type bukan application/pdf, fix it
if (fileName.endsWith(".pdf") && originalType !== "application/pdf") {
console.log("⚠️ WARNING: PDF file has wrong MIME type:", originalType);
console.log("🔧 Overriding to: application/pdf");
// Create new File with correct MIME type
const buffer = await file.arrayBuffer();
fixedFile = new File([buffer], file.name, {
type: "application/pdf",
lastModified: file.lastModified,
});
// Rebuild formData with fixed file
formData.set("file", fixedFile);
}
```
## 🧪 Cara Testing
### 1. **Test Upload dari Android**
Coba upload file PDF dari Android dan perhatikan log di terminal:
```bash
# Log yang akan muncul:
=== UPLOAD REQUEST START ===
dirId: xxx
File details:
- Name: dokumen.pdf
- Type: application/pdf
- Size: 1234567 bytes
- Size (KB): 1205.63
===========================
Directory key: xxx
=== EXTERNAL STORAGE RESPONSE ===
Status: 400
Status Text: Bad Request
Content-Type: text/html; charset=utf-8
=================================
=== ERROR: Non-JSON Response ===
Response text: Unsupported file format...
=================================
```
### 2. **Informasi yang Perlu Dicari:**
Dari log di atas, perhatikan:
- **File size** → Berapa MB? (mungkin terlalu besar?)
- **File type** → `application/pdf` atau yang lain?
- **External storage response** → Status code & message?
- **Error text** → Apa yang dikembalikan server external?
### 3. **Compare iOS vs Android**
Upload file yang sama dari iOS dan Android, bandingkan log-nya.
## 📊 Expected Log Output
### ✅ Success Case:
```
=== UPLOAD REQUEST START ===
dirId: investment
File details:
- Name: proposal.pdf
- Type: application/pdf
- Size: 524288 bytes
- Size (KB): 512.00
===========================
Directory key: investment
=== EXTERNAL STORAGE RESPONSE ===
Status: 200
Status Text: OK
Content-Type: application/json
=================================
✅ Upload SUCCESS
```
### ❌ Failed Case (Non-JSON Response):
```
=== UPLOAD REQUEST START ===
dirId: investment
File details:
- Name: proposal.pdf
- Type: application/pdf
- Size: 5242880 bytes ← Mungkin terlalu besar?
- Size (KB): 5120.00
===========================
=== EXTERNAL STORAGE RESPONSE ===
Status: 413 ← Payload Too Large?
Content-Type: text/html
=================================
=== ERROR: Non-JSON Response ===
Response text: <html><body>413 Request Entity Too Large</body></html>
=================================
```
## 🔧 Next Steps (Setelah Testing)
Berdasarkan log, kita bisa identify masalahnya:
### **Jika masalah FILE SIZE:**
- Tambahkan limit validation di frontend
- Compress PDF sebelum upload
- Increase external storage limit
### **Jika masalah FILE FORMAT:**
- Validate file type sebelum upload
- Convert format jika perlu
- Update external storage allowed formats
### **Jika masalah NETWORK/HEADERS:**
- Check user-agent differences
- Validate Authorization header
- Check CORS settings
## 📝 Checklist Testing
- [ ] Test upload PDF kecil (< 1MB) dari Android
- [ ] Test upload PDF besar (> 5MB) dari Android
- [ ] Test upload PDF dari iOS (baseline)
- [ ] Compare log output iOS vs Android
- [ ] Check file type yang dikirim
- [ ] Check file size yang dikirim
- [ ] Check response dari external storage
## 🎯 Goal
Dari log yang detail, kita bisa tahu **exact reason** kenapa Android fail, lalu fix dengan tepat.
---
**Last Updated:** 2026-04-14
**Status:** ✅ Logging added, ready for testing

68
Dockerfile Normal file
View File

@@ -0,0 +1,68 @@
# ==============================
# Stage 1: Builder
# ==============================
FROM oven/bun:1-debian AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libc6 \
git \
openssl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY package.json bun.lockb* ./
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN bun install --frozen-lockfile
COPY . .
RUN cp .env.example .env || true
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
RUN bunx prisma generate
RUN bun run build
# ==============================
# Stage 2: Runner (Production)
# ==============================
FROM oven/bun:1-debian AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
RUN apt-get update && apt-get install -y --no-install-recommends \
openssl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs nextjs
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/src ./src
COPY --from=builder /app/next.config.js ./next.config.js
COPY --from=builder /app/tsconfig.json ./tsconfig.json
RUN chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["bun", "start"]

View File

@@ -1,5 +1,5 @@
File utama: src/app/api/mobile/voting/[id]/[status]/route.ts
File utama: src/app/api/mobile/admin/forum/[id]/comment/route.ts
Terapkan pagination pada file "File utama" pada method GET
Analisa juga file "File utama", jika belum memiliki page dari seachParams maka terapkan. Juga pastikan take dan skip sudah sesuai dengan pagination. Buat default nya menjadi 10 untuk take data
@@ -17,4 +17,29 @@ Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additinal prompt -->
File refrensi: src/app/api/mobile/event/[id]/[status]/route.ts
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
<!-- Auto input promt -->
End-point: src/app/api/mobile/donation/route.ts
Buatkan auto input untuk method POST dengan data yang dibutuhkan sesuai dengan struktur data yang ada di file tersebut, tidak perlu "temporary" cukup data "permanent" dengan ketentuan sebagai berikut:
- authorId: string ( cmha6wb9w0001cfndwl9fcse6 )
- title: string
- target: number
- donasiMaster_DurasiId: number ( 3 )
- donasiMaster_KategoriId: number ( 3 )
- namaBank: string
- rekening: string
- imageId: number ( cm60j9q3m000xc9dc584v8rh8 )
Untuk sisa nya anda bisa bebas mengisi data tersebut.
<!-- COMMIT & PUSH -->
Branch: mobile-api/10-feb-26
Jalankan perintah ini: git checkout -b "Branch"
Setelah itu jalankan perintah ini: git add .
Setelah itu jalankan perintah ini: git commit -m "
<Berikan semua catatan perubahan pada branch ini, tampilan pada saya dan pastikan dalam bahasa indonesia. Saya akan cek baru saya akan berikan perintah push>
"
Setelah itu jalankan perintah ini: git push origin "Branch"

68
QWEN.md
View File

@@ -2,7 +2,7 @@
## Project Overview
HIPMI (Himpunan Pengusaha Muda Indonesia) is a comprehensive Next.js-based web application built for the Indonesian Young Entrepreneurs Association. The project is a sophisticated platform that provides multiple business functionalities including investment management, donations, events, job listings, forums, voting systems, and collaborative projects.
HIPMI (Himpunan Pengusaha Muya Indonesia) is a comprehensive Next.js-based web application built for the Indonesian Young Entrepreneurs Association. The project is a sophisticated platform that provides multiple business functionalities including investment management, donations, events, job listings, forums, voting systems, and collaborative projects.
### Key Technologies
- **Framework**: Next.js 13+ (with App Router)
@@ -120,14 +120,6 @@ The team follows a structured Git workflow:
- `style`: Styling changes
- `perf`: Performance improvements
### Code Standards
- TypeScript with strict mode enabled
- Component header comments with file description, creator, date
- Function comments with parameter and return value descriptions
- Custom type interface comments
- Error handling comments
- Complex logic comments
### Commit Message Format
```
type: Short description
@@ -140,6 +132,62 @@ Body:
References: #issue-number
```
**Example:**
```
feat: Tambahkan fitur kalkulator
Deskripsi:
- Menambahkan fungsi penambahan, pengurangan, perkalian, dan pembagian
- Memperbolehkan pengguna untuk memasukkan dua angka dan melakukan operasi matematika
Fixes #12
```
### Code Standards
- TypeScript with strict mode enabled
- Component header comments with file description, creator, date
- Function comments with parameter and return value descriptions
- Custom type interface comments
- Error handling comments
- Complex logic comments
### Comment Standards
**File Header:**
```typescript
/**
* Nama File: app.ts
* Deskripsi: Ini adalah file utama aplikasi.
* Pembuat: John Doe
* Tanggal: 27 Juli 2023
*/
```
**Function Comments:**
```typescript
/**
* Fungsi untuk menambahkan dua angka.
* @param {number} a - Angka pertama.
* @param {number} b - Angka kedua.
* @returns {number} Hasil penjumlahan a dan b.
*/
function addNumbers(a: number, b: number): number {
return a + b;
}
```
**Custom Type Comments:**
```typescript
/**
* Interface untuk merepresentasikan informasi pelanggan.
*/
interface Customer {
id: number; // ID pelanggan
name: string; // Nama pelanggan
age?: number; // Umur pelanggan (opsional)
}
```
## Project Structure
### Main Directories
@@ -198,4 +246,4 @@ References: #issue-number
### Data Protection
- Encrypted tokens
- Secure API routes
- Proper CORS configuration
- Proper CORS configuration

View File

@@ -39,6 +39,7 @@
"@types/react-dom": "18.2.7",
"@types/uuid": "^9.0.4",
"autoprefixer": "10.4.14",
"axios": "^1.13.5",
"bufferutil": "^4.0.8",
"bun": "^1.1.38",
"caniuse-lite": "^1.0.30001757",
@@ -1324,7 +1325,7 @@
"axe-core": ["axe-core@4.10.2", "", {}, "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w=="],
"axios": ["axios@0.26.1", "", { "dependencies": { "follow-redirects": "^1.14.8" } }, "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA=="],
"axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -1882,7 +1883,7 @@
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
"fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="],
@@ -1892,7 +1893,7 @@
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
@@ -3618,6 +3619,8 @@
"@types/jsonwebtoken/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
"@types/node-fetch/form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"@types/request/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
"@types/request/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
@@ -3630,6 +3633,8 @@
"ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"autocannon/form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001701", "", {}, "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw=="],
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -3810,6 +3815,8 @@
"metro-file-map/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"midtrans-client/axios": ["axios@0.26.1", "", { "dependencies": { "follow-redirects": "^1.14.8" } }, "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -4190,6 +4197,12 @@
"metro/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"midtrans-client/axios/follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"next-dev/axios/follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"next-dev/axios/form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"next-dev/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"next-scroll-loader/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],

View File

@@ -1,16 +0,0 @@
import { fileURLToPath } from "url";
import { dirname } from "path"; // Pastikan `dirname` diimpor
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@@ -1,27 +1,36 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
output: "standalone",
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
experimental: {
serverActions: true,
serverComponentsExternalPackages: ["@prisma/client", ".prisma/client"],
},
output: "standalone",
staticPageGenerationTimeout: 180, // tingkatkan menjadi 3 menit
eslint: {
ignoreDuringBuilds: true,
webpack: (config, { isServer }) => {
if (isServer) {
config.externals = config.externals || [];
config.externals.push("@prisma/client");
config.externals.push(".prisma/client");
}
return config;
},
async headers() {
return [
{
source: "/.well-known/:path*",
headers: [
{ key: "Content-Type", value: "application/json" },
{
key: "Cache-Control",
value: "no-cache, no-store, must-revalidate",
},
],
},
];
},
// async headers() {
// return [
// {
// source: "/(.*)",
// headers: [
// {
// key: "Cache-Control",
// value: "no-store, max-age=0",
// },
// ],
// },
// ];
// },
};
module.exports = nextConfig;

View File

@@ -1,15 +1,16 @@
{
"name": "hipmi",
"version": "1.5.39",
"version": "1.7.5",
"private": true,
"prisma": {
"seed": "bun prisma/seed.ts"
},
"scripts": {
"dev": "next dev --experimental-https",
"build": "next build",
"build:dev": "next build",
"build": "prisma generate && next build",
"build:dev": "prisma generate && next build",
"start": "next start",
"postbuild": "node scripts/postbuild.js",
"lint": "next lint",
"ver": "bunx commit-and-tag-version -- --prerelease"
},
@@ -50,6 +51,7 @@
"@types/react-dom": "18.2.7",
"@types/uuid": "^9.0.4",
"autoprefixer": "10.4.14",
"axios": "^1.13.5",
"bufferutil": "^4.0.8",
"bun": "^1.1.38",
"caniuse-lite": "^1.0.30001757",

View File

@@ -4,7 +4,7 @@
generator client {
provider = "prisma-client-js"
engineType = "binary"
binaryTargets = ["native"]
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
}
datasource db {

View File

@@ -13,6 +13,6 @@ import { generate_seeder } from "./../src/app_modules/_global/fun/generate_seede
console.error("<< error seeder", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
// .finally(async () => {
// await prisma.$disconnect();
// });

View File

@@ -1,8 +1,10 @@
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.bip.hipmimobileapp",
"sha256_cert_fingerprints": ["CFF8431520BFAE665025B68138774A4E64AA6338D2DF6C7D900A71F0551FFD2D"]
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.bip.hipmimobileapp",
"sha256_cert_fingerprints": ["CFF8431520BFAE665025B68138774A4E64AA6338D2DF6C7D900A71F0551FFD2D"]
}
}
}]
]

60
scripts/postbuild.js Normal file
View File

@@ -0,0 +1,60 @@
const fs = require('fs');
const path = require('path');
const standaloneDir = path.join(__dirname, '../.next/standalone');
const prismaDir = path.join(__dirname, '../node_modules/.prisma');
console.log('🚀 Running postbuild script...');
// Copy Prisma binaries ke standalone output
if (fs.existsSync(prismaDir)) {
const dest = path.join(standaloneDir, 'node_modules/.prisma');
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.cpSync(prismaDir, dest, { recursive: true });
console.log('✓ Prisma binaries copied to standalone output');
} else {
console.warn('⚠ Prisma binaries directory not found, skipping...');
}
// Copy schema.prisma jika diperlukan
const schemaSrc = path.join(__dirname, '../prisma/schema.prisma');
const schemaDest = path.join(standaloneDir, 'prisma/schema.prisma');
if (fs.existsSync(schemaSrc)) {
fs.mkdirSync(path.dirname(schemaDest), { recursive: true });
fs.copyFileSync(schemaSrc, schemaDest);
console.log('✓ schema.prisma copied to standalone output');
} else {
console.warn('⚠ schema.prisma not found, skipping...');
}
// Copy prisma client dari node_modules/@prisma/client
const prismaClientSrc = path.join(__dirname, '../node_modules/@prisma/client');
const prismaClientDest = path.join(standaloneDir, 'node_modules/@prisma/client');
if (fs.existsSync(prismaClientSrc)) {
fs.mkdirSync(path.dirname(prismaClientDest), { recursive: true });
fs.cpSync(prismaClientSrc, prismaClientDest, { recursive: true });
console.log('✓ @prisma/client copied to standalone output');
} else {
console.warn('⚠ @prisma/client not found, skipping...');
}
// Copy .env file jika ada (untuk production)
const envSrc = path.join(__dirname, '../.env');
const envDest = path.join(standaloneDir, '.env');
if (fs.existsSync(envSrc)) {
fs.copyFileSync(envSrc, envDest);
// console.log('✓ .env file copied to standalone output');
} else {
// console.warn('⚠ .env file not found, skipping...');
console.warn(' Pastikan DATABASE_URL di-set di system environment server!');
}
// Copy .env-local file jika ada (opsional)
const envLocalSrc = path.join(__dirname, '../.env-local');
const envLocalDest = path.join(standaloneDir, '.env-local');
if (fs.existsSync(envLocalSrc)) {
fs.copyFileSync(envLocalSrc, envLocalDest);
// console.log('✓ .env-local file copied to standalone output');
}
console.log('✅ Build script completed!');

View File

@@ -0,0 +1,186 @@
/**
* Route Handler untuk Deep Link Event Confirmation
* File: app/event/[id]/confirmation/route.ts
* Deskripsi: Handle GET request untuk deep link event confirmation dengan redirect ke mobile app
* Pembuat: Assistant
* Tanggal: 9 Maret 2026
*/
import { url } from "inspector";
import { NextRequest, NextResponse } from "next/server";
/**
* Detect platform dari User Agent string
* @param userAgent - User Agent string dari request
* @returns Platform type: 'ios', 'android', atau 'web'
*/
function detectPlatform(userAgent: string | null): "ios" | "android" | "web" {
if (!userAgent) {
return "web";
}
const lowerUA = userAgent.toLowerCase();
// Detect iOS devices
if (
/iphone|ipad|ipod/.test(lowerUA) ||
(lowerUA.includes("mac") && (("ontouchend" in {}) as any))
) {
return "ios";
}
// Detect Android devices
if (/android/.test(lowerUA)) {
return "android";
}
// Default to web
return "web";
}
/**
* Build custom scheme URL untuk mobile app
* @param eventId - Event ID dari URL
* @param userId - User ID dari query parameter
* @param platform - Platform yang terdetect
* @returns Custom scheme URL
*/
function buildCustomSchemeUrl(
eventId: string,
userId: string | null,
platform: "ios" | "android" | "web",
): string {
const baseUrl = "hipmimobile://event";
const url = `${baseUrl}/${eventId}/confirmation${userId ? `?userId=${userId}` : ""}`;
return url;
}
/**
* Get base URL dari environment
*/
function getBaseUrl(): string {
const env = process.env.NEXT_PUBLIC_ENV || "development";
if (env === "production") {
return "https://hipmi.muku.id";
}
if (env === "staging") {
return "https://cld-dkr-hipmi-stg.wibudev.com";
}
return "http://localhost:3000";
}
/**
* Handle GET request untuk deep link
* @param request - Next.js request object
* @returns Redirect ke mobile app atau JSON response untuk debugging
*/
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } },
) {
try {
// Parse query parameters
const searchParams = request.nextUrl.searchParams;
const eventId = params.id;
const userId = searchParams.get("userId") || null;
const userAgent = request.headers.get("user-agent") || "";
// Detect platform
const platform = detectPlatform(userAgent);
// Log untuk tracking
console.log("[Deep Link] Event Confirmation Received:", {
eventId,
userId,
platform,
userAgent:
userAgent.substring(0, 100) + (userAgent.length > 100 ? "..." : ""),
timestamp: new Date().toISOString(),
url: request.url,
});
// Build custom scheme URL untuk redirect
const customSchemeUrl = buildCustomSchemeUrl(eventId, userId, platform);
// Redirect ke mobile app untuk iOS dan Android
if (platform === "ios" || platform === "android") {
console.log("[Deep Link] Redirecting to mobile app:", customSchemeUrl);
// Redirect ke custom scheme URL
return NextResponse.redirect(customSchemeUrl);
}
console.log("[Deep Link] Environment:", process.env.NEXT_PUBLIC_ENV);
console.log("[Deep Link] Base URL:", getBaseUrl());
console.log("[Deep Link] Request:", {
eventId,
userId,
platform,
url: request.url,
timestamp: new Date().toISOString(),
});
// Untuk web/desktop, tampilkan JSON response untuk debugging
const responseData = {
success: true,
message: "Deep link received - Web fallback",
data: {
eventId,
userId,
platform,
userAgent,
timestamp: new Date().toISOString(),
url: request.url,
customSchemeUrl,
note: "This is a web fallback. Mobile users will be redirected to the app.",
},
};
return NextResponse.json(responseData, {
status: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
} catch (error) {
console.error("[Deep Link] Error processing request:", error);
return NextResponse.json(
{
success: false,
message: "Error processing deep link",
error: error instanceof Error ? error.message : "Unknown error",
},
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
},
);
}
}
/**
* Handle OPTIONS request untuk CORS preflight
*/
export async function OPTIONS() {
return NextResponse.json(
{},
{
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
},
);
}

View File

@@ -98,7 +98,7 @@ export default function SupportCenter() {
<Title>Support Center</Title>
</Group>
<Text align="center">
Send us a message and we'll get back to you as soon as possible.
Send us a message and we&apos;ll get back to you as soon as possible.
</Text>
</Stack>
</Stack>

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib";
export const dynamic = "force-dynamic";
@@ -16,13 +17,25 @@ export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const userId = searchParams.get("id");
const data = await prisma.notifikasi.count({
where: {
adminId: userId,
userRoleId: "2",
isRead: false,
},
});
if (!userId) {
return NextResponse.json(
{ success: false, message: "User ID is required" },
{ status: 400 }
);
}
const data = await withRetry(
() =>
prisma.notifikasi.count({
where: {
adminId: userId,
userRoleId: "2",
isRead: false,
},
}),
undefined,
"countAdminNotifications"
);
return NextResponse.json(
{
@@ -33,7 +46,25 @@ export async function GET(request: Request) {
{ status: 200 }
);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("Error get count notifikasi", error);
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
return NextResponse.json(
{
success: false,
message: "Database connection error. Please try again.",
data: null,
},
{ status: 503 }
);
}
return NextResponse.json(
{
success: false,

View File

@@ -50,7 +50,5 @@ async function DELETE(
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -1,3 +1,4 @@
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib";
import { randomOTP } from "@/app_modules/auth/fun/rondom_otp";
import { NextResponse } from "next/server";
@@ -9,11 +10,26 @@ export async function POST(req: Request) {
const body = await req.json();
const { nomor } = body;
const user = await prisma.user.findUnique({
where: {
nomor: nomor,
},
});
if (!nomor) {
return NextResponse.json(
{
success: false,
message: "Nomor telepon diperlukan",
status: 400,
}
);
}
const user = await withRetry(
() =>
prisma.user.findUnique({
where: {
nomor: nomor,
},
}),
undefined,
"findUserByNomor"
);
if (!user)
return NextResponse.json({
@@ -22,12 +38,17 @@ export async function POST(req: Request) {
status: 404,
});
const createOtpId = await prisma.kodeOtp.create({
data: {
nomor: nomor,
otp: codeOtp,
},
});
const createOtpId = await withRetry(
() =>
prisma.kodeOtp.create({
data: {
nomor: nomor,
otp: codeOtp,
},
}),
undefined,
"createOTP"
);
if (!createOtpId)
return NextResponse.json(
@@ -59,6 +80,25 @@ export async function POST(req: Request) {
{ status: 200 },
);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("Mobile login error:", error);
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
return NextResponse.json(
{
success: false,
message: "Database connection error. Please try again.",
status: 503,
},
{ status: 503 }
);
}
return NextResponse.json(
{
success: false,

View File

@@ -1,8 +1,12 @@
import { sessionCreate } from "@/app/(auth)/_lib/session_create";
import prisma from "@/lib/prisma";
import backendLogger from "@/util/backendLogger";
import { NextResponse } from "next/server";
/**
* Validasi OTP untuk login mobile
* @param req - Request dengan body { nomor: string, code: string }
* @returns Response dengan token jika OTP valid
*/
export async function POST(req: Request) {
if (req.method !== "POST") {
return NextResponse.json(
@@ -12,8 +16,21 @@ export async function POST(req: Request) {
}
try {
const { nomor } = await req.json();
const { nomor, code } = await req.json();
// Validasi input: nomor dan code wajib ada
if (!nomor || !code) {
return NextResponse.json(
{ success: false, message: "Nomor dan kode OTP wajib diisi" },
{ status: 400 }
);
}
// Special case untuk Apple Review: nomor 6282340374412 dengan code "1234" selalu valid
const isAppleReviewNumber = nomor === "6282340374412";
const isAppleReviewCode = code === "1234";
// Cek user berdasarkan nomor
const dataUser = await prisma.user.findUnique({
where: {
nomor: nomor,
@@ -28,11 +45,92 @@ export async function POST(req: Request) {
},
});
if (dataUser == null)
if (dataUser == null) {
return NextResponse.json(
{ success: false, message: "Nomor Belum Terdaftar" },
{ status: 200 }
);
}
// Validasi OTP (skip untuk Apple Review number di production)
let otpValid = false;
if (isAppleReviewNumber && isAppleReviewCode) {
// Special case: Apple Review number dengan code "1234" selalu valid
otpValid = true;
console.log("Apple Review login bypass untuk nomor: " + nomor);
} else {
// Normal flow: validasi OTP dari database
const otpRecord = await prisma.kodeOtp.findFirst({
where: {
nomor: nomor,
isActive: true,
},
orderBy: {
createdAt: "desc",
},
});
if (!otpRecord) {
return NextResponse.json(
{ success: false, message: "Kode OTP tidak ditemukan" },
{ status: 400 }
);
}
// Cek expired OTP (5 menit dari createdAt)
const now = new Date();
const otpCreatedAt = new Date(otpRecord.createdAt);
const expiredTime = new Date(otpCreatedAt.getTime() + 5 * 60 * 1000); // 5 menit
if (now > expiredTime) {
// OTP sudah expired, update isActive menjadi false
await prisma.kodeOtp.updateMany({
where: {
nomor: nomor,
isActive: true,
},
data: {
isActive: false,
},
});
return NextResponse.json(
{ success: false, message: "Kode OTP sudah kadaluarsa" },
{ status: 400 }
);
}
// Validasi code OTP
const inputCode = parseInt(code);
if (isNaN(inputCode) || inputCode !== otpRecord.otp) {
return NextResponse.json(
{ success: false, message: "Kode OTP tidak valid" },
{ status: 400 }
);
}
otpValid = true;
// Nonaktifkan OTP yang sudah digunakan
await prisma.kodeOtp.updateMany({
where: {
nomor: nomor,
isActive: true,
},
data: {
isActive: false,
},
});
}
// Generate token jika OTP valid
if (!otpValid) {
return NextResponse.json(
{ success: false, message: "Validasi OTP gagal" },
{ status: 400 }
);
}
const token = await sessionCreate({
sessionKey: process.env.NEXT_PUBLIC_BASE_SESSION_KEY!,
@@ -46,6 +144,7 @@ export async function POST(req: Request) {
{ status: 500 }
);
}
// Buat response dengan token dalam cookie
const response = NextResponse.json(
{
@@ -69,7 +168,7 @@ export async function POST(req: Request) {
return response;
} catch (error) {
backendLogger.log("API Error or Server Error", error);
console.log("API Error or Server Error", error);
return NextResponse.json(
{
success: false,

View File

@@ -14,8 +14,6 @@ export async function POST(req: Request) {
try {
const { data } = await req.json();
console.log("data >>", data);
const cekUsername = await prisma.user.findUnique({
where: {
username: data.username,
@@ -29,12 +27,12 @@ export async function POST(req: Request) {
});
// ✅ Validasi wajib setuju Terms
if (data.termsOfServiceAccepted !== true) {
return NextResponse.json({
success: false,
message: "You must agree to the Terms of Service",
});
}
// if (data.termsOfServiceAccepted !== true) {
// return NextResponse.json({
// success: false,
// message: "You must agree to the Terms of Service",
// });
// }
const createUser = await prisma.user.create({
data: {

View File

@@ -64,7 +64,5 @@ export async function POST(req: Request) {
},
{ status: 500 },
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -42,7 +42,5 @@ export async function GET(
{ success: false, message: "Gagal mendapatkan data" },
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -30,13 +30,11 @@ export async function GET(request: Request) {
fixData = false;
}
await prisma.$disconnect();
return NextResponse.json(
{ success: true, message: "Success get data", data: fixData },
{ status: 200 }
);
} catch (error) {
await prisma.$disconnect();
backendLogger.error("Error get data detail event:", error);
return NextResponse.json(
{

View File

@@ -41,13 +41,11 @@ export async function POST(
},
});
await prisma.$disconnect();
return NextResponse.json({
success: true,
message: "Success create sponsor",
});
} catch (error) {
await prisma.$disconnect();
backendLogger.error("Error create sponsor event", error);
return NextResponse.json(
{ success: false, message: "Failed create sponsor" },
@@ -100,7 +98,5 @@ export async function GET(
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -58,7 +58,6 @@ export async function GET(
});
}
await prisma.$disconnect();
return NextResponse.json({
success: true,
message: "Success create sponsor",
@@ -66,7 +65,6 @@ export async function GET(
});
} catch (error) {
backendLogger.error("Error get sponsor event", error);
await prisma.$disconnect();
return NextResponse.json(
{
success: false,

View File

@@ -33,7 +33,5 @@ export async function GET(request: Request) {
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -35,7 +35,5 @@ export async function GET(request: Request) {
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -21,13 +21,11 @@ export async function GET(request: Request) {
},
});
await prisma.$disconnect();
return NextResponse.json(
{ success: true, message: "Berhasil mendapatkan data", data: res },
{ status: 200 }
);
} catch (error) {
await prisma.$disconnect();
backendLogger.error("Error Get Master Status Transaksi >>", error);
return NextResponse.json(
{

View File

@@ -10,6 +10,7 @@ import {
NotificationMobileTitleType,
} from "../../../../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET };
@@ -154,7 +155,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
console.log("[CATEGORY]", category);
@@ -174,6 +175,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
id: true,
createdAt: true,
nominalCair: true,
title: true,
},
});
} else if (category === "get-one") {

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -9,7 +10,7 @@ async function GET(req: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(req.url);
const page = searchParams.get("page");
const status = searchParams.get("status");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
const fixStatus = _.startCase(status || "");

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -9,11 +10,10 @@ async function GET(request: Request) {
const category = searchParams.get("category");
const page = searchParams.get("page");
const search = searchParams.get("search");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
console.log("[CATEGORY]", category);
let fixData;
try {
if (category === "dashboard") {
const publish = await prisma.donasi.count({
@@ -48,7 +48,7 @@ async function GET(request: Request) {
where: {
active: true,
},
}
},
);
const categoryDonation = countCategoryDonation.length;
@@ -68,7 +68,6 @@ async function GET(request: Request) {
},
});
console.log("[STATUS]", checkStatus);
if (!checkStatus) {
return NextResponse.json(
@@ -77,7 +76,7 @@ async function GET(request: Request) {
message: "Failed to get data donation",
reason: "Status not found",
},
{ status: 500 }
{ status: 500 },
);
}
@@ -100,6 +99,12 @@ async function GET(request: Request) {
select: {
id: true,
title: true,
target: true,
DonasiMaster_Durasi: {
select: {
name: true,
},
},
Author: {
select: {
id: true,
@@ -109,7 +114,6 @@ async function GET(request: Request) {
},
});
console.log("[LIST]", fixData);
}
return NextResponse.json(
@@ -118,7 +122,7 @@ async function GET(request: Request) {
message: `Success get data donation ${category}`,
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.error("Error get data donation:", error);
@@ -128,7 +132,7 @@ async function GET(request: Request) {
message: "Failed to get data donation",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -1,9 +1,15 @@
import { NextResponse } from "next/server";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
export { GET };
async function GET(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
try {
const { id } = params;
@@ -12,6 +18,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
eventId: id,
},
select: {
id: true,
eventId: true,
userId: true,
isPresent: true,
@@ -35,6 +42,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
return NextResponse.json(

View File

@@ -1,7 +1,8 @@
import _ from "lodash";
import { prisma } from "@/lib";
import { NextResponse } from "next/server";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import _ from "lodash";
import moment from "moment";
import { NextResponse } from "next/server";
export { GET };
@@ -11,13 +12,12 @@ async function GET(request: Request) {
const fixStatus = _.startCase(category || "");
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const skipData = Number(page) * takeData - takeData;
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
console.log("[CATEGORY]", category);
// console.log("[FIX STATUS]", fixStatus);
try {
if (category === "dashboard") {
@@ -71,7 +71,6 @@ async function GET(request: Request) {
typeOfEvent,
};
} else if (category === "history") {
console.log("[HISTORY HERE]");
const data = await prisma.event.findMany({
take: page ? takeData : undefined,
@@ -151,21 +150,22 @@ async function GET(request: Request) {
},
},
select: {
id: true,
title: true,
tanggal: true,
Author: {
select: {
id: true,
username: true,
Profile: {
select: {
name: true,
},
id: true,
title: true,
tanggal: true,
tanggalSelesai: true,
Author: {
select: {
id: true,
username: true,
Profile: {
select: {
name: true,
},
},
},
},
},
});
fixData = data;
@@ -177,7 +177,7 @@ async function GET(request: Request) {
message: `Success get data event ${category}`,
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.log(`[ERROR GET DATA EVENT: ${category}]`, error);
@@ -187,7 +187,7 @@ async function GET(request: Request) {
message: `Error get data event ${category}`,
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -7,6 +7,7 @@ import {
NotificationMobileTitleType,
} from "../../../../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, PUT };
@@ -14,9 +15,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const skipData = Number(page) * takeData - takeData;
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const category = searchParams.get("category");
let fixData;
try {

View File

@@ -1,15 +1,16 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
{ params }: { params: { id: string } },
) {
const { id } = params;
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
let fixData;
@@ -60,7 +61,7 @@ export async function GET(
message: "Success get list report posting",
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.error("[ERROR GET LIST REPORT POSTING]", error);
@@ -70,7 +71,7 @@ export async function GET(
message: "Error get list report posting",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import _ from "lodash";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -9,7 +10,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
const category = searchParams.get("category");
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
let fixData;
@@ -79,7 +80,11 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
_count: {
select: {
Forum_ReportPosting: true,
Forum_Komentar: true,
Forum_Komentar: {
where: {
isActive: true,
},
},
},
},
},
@@ -139,6 +144,14 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
},
});
// Hitung count report untuk setiap Forum_Posting id
const countByPostingId = data.reduce((acc: any, item: any) => {
const key = item.Forum_Posting?.id;
if (!key) return acc;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const filterLatest = (data: any) =>
Object.values(
data.reduce((acc: any, item: any) => {
@@ -151,10 +164,16 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
acc[key] = item;
}
return acc;
}, {})
}, {}),
);
fixData = filterLatest(data);
const filteredData = filterLatest(data);
// Tambahkan count ke setiap item
fixData = filteredData.map((item: any) => ({
...item,
count: countByPostingId[item.Forum_Posting?.id] || 0,
}));
} else if (category === "report_comment") {
const data = await prisma.forum_ReportKomentar.findMany({
take: page ? takeData : undefined,
@@ -193,6 +212,14 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
},
});
// Hitung count report untuk setiap Forum_Komentar id
const countByKomentarId = data.reduce((acc: any, item: any) => {
const key = item.Forum_Komentar?.id;
if (!key) return acc;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const filterLatest = (data: any) =>
Object.values(
data.reduce((acc: any, item: any) => {
@@ -205,10 +232,16 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
acc[key] = item;
}
return acc;
}, {})
}, {}),
);
fixData = filterLatest(data);
const filteredData = filterLatest(data);
// Tambahkan count ke setiap item
fixData = filteredData.map((item: any) => ({
...item,
count: countByKomentarId[item.Forum_Komentar?.id] || 0,
}));
} else {
return NextResponse.json(
{
@@ -216,7 +249,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: "Invalid category",
reason: "Invalid category",
},
{ status: 400 }
{ status: 400 },
);
}
@@ -226,7 +259,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: `Success get data forum ${category}`,
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
@@ -235,7 +268,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: `Error get data forum ${category}`,
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -1,19 +1,20 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
{ params }: { params: { id: string } },
) {
try {
let fixData;
const { id } = params;
const { searchParams } = new URL(request.url);
const page = searchParams.get("page");
const page = Number(searchParams.get("page"));
const status = searchParams.get("status");
const takeData = 10;
const skipData = Number(page) * takeData - takeData;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const fixStatus = _.startCase(status ? status : "");
@@ -43,6 +44,7 @@ export async function GET(
id: true,
Author: true,
StatusInvoice: true,
nominal: true,
},
});
@@ -54,7 +56,7 @@ export async function GET(
message: "Success get status transaksi",
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.error("Eror get status transaksi", error);
@@ -64,7 +66,7 @@ export async function GET(
message: "Error get status transaksi",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -9,12 +10,9 @@ async function GET(request: Request) {
const category = searchParams.get("category");
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
console.log("[CATEGORY]", category);
console.log("[PAGE]", page);
let fixData;
try {
if (category === "dashboard") {
@@ -49,7 +47,6 @@ async function GET(request: Request) {
};
} else {
const fixCategoryToStatus = _.startCase(category || "");
console.log("[STATUS]", fixCategoryToStatus);
const data = await prisma.investasi.findMany({
take: page ? takeData : undefined,
@@ -70,6 +67,12 @@ async function GET(request: Request) {
select: {
id: true,
title: true,
targetDana: true,
MasterPencarianInvestor: {
select: {
name: true,
},
},
author: {
select: {
id: true,

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -8,6 +9,9 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const search = searchParams.get("search");
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
try {
@@ -66,6 +70,8 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
title: true,
Author: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
}

View File

@@ -1,14 +1,22 @@
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import { NextResponse } from "next/server";
export { GET, POST };
async function GET() {
async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const data = await prisma.masterBank.findMany({
orderBy: {
updatedAt: "desc",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
return NextResponse.json(

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import { NextResponse } from "next/server";
export { GET, PUT };
@@ -11,6 +12,10 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const category = searchParams.get("category");
const subBidangId = searchParams.get("subBidangId");
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
if (category === "all") {
const bidang = await prisma.masterBidangBisnis.findUnique({
where: {
@@ -45,6 +50,16 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
});
fixData = subBidang;
} else if (category === "only-sub-bidang") {
const subBidang = await prisma.masterSubBidangBisnis.findMany({
where: {
masterBidangBisnisId: id,
},
take: takeData,
skip: skipData,
});
fixData = subBidang;
}
@@ -71,9 +86,6 @@ async function PUT(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
console.log("category", category);
console.log("data", data);
try {
if (category === "bidang") {
const updateData = await prisma.masterBidangBisnis.update({

View File

@@ -2,15 +2,24 @@ import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import _ from "lodash";
import { Prisma } from "@prisma/client";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, POST };
async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const data = await prisma.masterBidangBisnis.findMany({
orderBy: {
createdAt: "asc",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
return NextResponse.json({

View File

@@ -1,10 +1,14 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, POST };
async function GET(request: Request) {
async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
// const category = searchParams.get("category");
let fixData;
@@ -13,6 +17,8 @@ async function GET(request: Request) {
orderBy: {
createdAt: "asc",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
// if (category === "category") {

View File

@@ -1,14 +1,22 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, POST };
async function GET(request: Request) {
async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const data = await prisma.eventMaster_TipeAcara.findMany({
orderBy: {
updatedAt: "desc",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
return NextResponse.json({

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import { NextResponse } from "next/server";
export { GET };
@@ -7,10 +8,16 @@ async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const category = searchParams.get("category");
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
console.log("SEARCH", search);
console.log("PAGE", page);
let fixData;
try {
if(category === "only-user"){
if (category === "only-user") {
fixData = await prisma.user.findMany({
orderBy: {
updatedAt: "desc",
@@ -22,8 +29,10 @@ async function GET(request: Request) {
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
} else if(category === "only-admin"){
} else if (category === "only-admin") {
fixData = await prisma.user.findMany({
orderBy: {
updatedAt: "desc",
@@ -35,8 +44,10 @@ async function GET(request: Request) {
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
} else if (category === "all-role"){
} else if (category === "all-role") {
fixData = await prisma.user.findMany({
orderBy: {
updatedAt: "desc",
@@ -48,13 +59,15 @@ async function GET(request: Request) {
},
{
masterUserRoleId: "2",
}
},
],
username: {
contains: search || "",
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
}
@@ -65,13 +78,11 @@ async function GET(request: Request) {
data: fixData,
});
} catch (error) {
return NextResponse.json(
{
status: 500,
success: false,
message: "Error get data user access",
reason: (error as Error).message,
},
);
return NextResponse.json({
status: 500,
success: false,
message: "Error get data user access",
reason: (error as Error).message,
});
}
}

View File

@@ -2,6 +2,7 @@ import _ from "lodash";
import moment from "moment";
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -12,7 +13,7 @@ async function GET(request: Request) {
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
let fixData;

View File

@@ -4,13 +4,42 @@ import { prisma } from "@/lib";
export { POST, GET };
async function POST(request: NextRequest) {
const { data } = await request.json();
try {
// Parse the request body - can accept either nested under 'data' or directly
const requestBody = await request.json();
// Check if the data is nested under 'data' property (as described in the issue)
// or if it's directly in the request body (more common pattern)
const payload = requestBody.data ? requestBody.data : requestBody;
const { userId, platform, deviceId, model, appVersion, fcmToken } = payload;
const { userId, platform, deviceId, model, appVersion, fcmToken } = data;
// Validate required fields
if (!fcmToken) {
return NextResponse.json({ error: "Missing Token" }, { status: 400 });
return NextResponse.json(
{ error: "Missing FCM token", field: "fcmToken" },
{ status: 400 }
);
}
if (!userId) {
return NextResponse.json(
{ error: "Missing user ID", field: "userId" },
{ status: 400 }
);
}
// Verify that the user exists before creating/updating the device token
const userExists = await prisma.user.findUnique({
where: { id: userId },
select: { id: true }
});
if (!userExists) {
return NextResponse.json(
{ error: "User not found", field: "userId" },
{ status: 404 }
);
}
const existing = await prisma.tokenUserDevice.findFirst({
@@ -23,7 +52,6 @@ async function POST(request: NextRequest) {
},
});
console.log("✅ EX", existing);
let deviceToken;
@@ -31,7 +59,7 @@ async function POST(request: NextRequest) {
if (existing) {
deviceToken = await prisma.tokenUserDevice.update({
where: {
id: existing?.id,
id: existing.id,
},
data: {
platform,
@@ -43,7 +71,7 @@ async function POST(request: NextRequest) {
},
});
} else {
// Buat baru jika belum ada
// Create new device token record
deviceToken = await prisma.tokenUserDevice.create({
data: {
token: fcmToken,
@@ -58,9 +86,16 @@ async function POST(request: NextRequest) {
}
return NextResponse.json({ success: true, data: deviceToken });
} catch (error) {
} catch (error: any) {
console.error("Error registering device token:", error);
// Return more informative error response
return NextResponse.json(
{ error: (error as Error).message },
{
error: "Internal server error",
message: error.message || "An unexpected error occurred",
field: "server"
},
{ status: 500 }
);
}

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, PUT };
@@ -10,8 +11,14 @@ async function GET(
) {
const { id, status } = params;
const fixStatus = _.startCase(status);
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData;
let fixData;
let meta = null;
try {
const checkStatus = await prisma.donasiMaster_StatusDonasi.findFirst({
where: {
@@ -50,18 +57,38 @@ async function GET(
orderBy: {
updatedAt: "desc",
},
take: takeData,
skip: skipData,
});
const totalData = await prisma.donasi.count({
where: {
authorId: id,
donasiMaster_StatusDonasiId: checkStatus.id,
active: true,
},
});
const totalPages = Math.ceil(totalData / takeData);
fixData = res.map((v: any) => ({
..._.omit(v, ["DonasiMaster_Durasi"]),
nameDonasiDurasi: v.DonasiMaster_Durasi.name,
}));
meta = {
currentPage: page,
totalData: totalData,
totalPage: totalPages,
dataPerPage: takeData,
};
return NextResponse.json({
status: 200,
success: true,
message: "Berhasil mendapatkan data",
data: fixData,
...(meta && { meta }),
});
} catch (error) {
console.log("[ERROR]", error);

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -7,7 +8,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = 5;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
try {

View File

@@ -1,3 +1,4 @@
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
@@ -9,11 +10,12 @@ export async function GET(
let fixData;
const { id } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = 10;
const page = Number(searchParams.get("page")) || 1; // Default page 1 jika tidak ada atau invalid
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
fixData = await prisma.donasi_Invoice.findMany({
// Query data dengan pagination
const data = await prisma.donasi_Invoice.findMany({
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
orderBy: {
@@ -59,10 +61,31 @@ export async function GET(
},
});
// Hitung total data untuk pagination
const totalCount = await prisma.donasi_Invoice.count({
where: {
donasiId: id,
DonasiMaster_StatusInvoice: {
name: "Berhasil",
},
},
});
// Hitung total halaman
const totalPages = Math.ceil(totalCount / takeData);
fixData = data;
return NextResponse.json({
success: true,
message: "Data berhasil diambil",
data: fixData,
pagination: {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
},
});
} catch (error) {
return NextResponse.json({

View File

@@ -8,6 +8,7 @@ import {
} from "../../../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { funFindDonaturList } from "@/lib/mobile/donation/find-donatur-list";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET, PUT, DELETE };
@@ -94,11 +95,16 @@ async function GET(
const { id } = params;
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = Number(searchParams.get("page")) || 1; // Default page 1 jika tidak ada
const takeData = PAGINATION_DEFAULT_TAKE; // Default 10 data per halaman
const skipData = page * takeData - takeData;
let fixData;
let totalCount = 0; // Untuk menghitung total data
try {
if (category === "get-all") {
fixData = await prisma.donasi_Kabar.findMany({
const data = await prisma.donasi_Kabar.findMany({
orderBy: {
updatedAt: "desc",
},
@@ -106,6 +112,8 @@ async function GET(
donasiId: id,
active: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
title: true,
@@ -113,6 +121,17 @@ async function GET(
createdAt: true,
},
});
// Hitung total data untuk pagination
totalCount = await prisma.donasi_Kabar.count({
where: {
donasiId: id,
active: true,
},
});
fixData = data;
} else if (category === "get-one") {
const data = await prisma.donasi_Kabar.findUnique({
where: {
@@ -135,11 +154,24 @@ async function GET(
};
}
// Hitung total halaman jika kategori adalah get-all
let pagination = undefined;
if (category === "get-all") {
const totalPages = Math.ceil(totalCount / takeData);
pagination = {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
};
}
return NextResponse.json({
status: 200,
success: true,
message: "Berhasil mengambil kabar",
data: fixData,
pagination: pagination,
});
} catch (error) {
console.error("[ERROR GET NEWS]", error);

View File

@@ -4,6 +4,7 @@ import _ from "lodash";
import { NextResponse } from "next/server";
import { NotificationMobileBodyType } from "../../../../../types/type-mobile-notification";
import { routeAdminMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET };
@@ -125,7 +126,12 @@ async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const authorId = searchParams.get("authorId");
const page = Number(searchParams.get("page")) || 1; // Default page 1 jika tidak ada
const takeData = PAGINATION_DEFAULT_TAKE; // Default 10 data per halaman
const skipData = page * takeData - takeData;
let fixData;
let totalCount = 0; // Untuk menghitung total data
try {
if (category === "beranda") {
@@ -137,6 +143,8 @@ async function GET(request: Request) {
donasiMaster_StatusDonasiId: "1",
active: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
imageId: true,
@@ -152,6 +160,14 @@ async function GET(request: Request) {
},
});
// Hitung total data untuk pagination
totalCount = await prisma.donasi.count({
where: {
donasiMaster_StatusDonasiId: "1",
active: true,
},
});
fixData = data.map((v: any) => ({
..._.omit(v, ["DonasiMaster_Durasi"]),
durasiDonasi: v.DonasiMaster_Durasi.name,
@@ -164,6 +180,8 @@ async function GET(request: Request) {
where: {
authorId: authorId,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
nominal: true,
@@ -190,6 +208,13 @@ async function GET(request: Request) {
},
});
// Hitung total data untuk pagination
totalCount = await prisma.donasi_Invoice.count({
where: {
authorId: authorId,
},
});
fixData = data.map((v: any) => ({
..._.omit(v, ["DonasiMaster_StatusInvoice", "Donasi"]),
statusInvoice: v.DonasiMaster_StatusInvoice.name,
@@ -202,8 +227,21 @@ async function GET(request: Request) {
}));
}
// Hitung total halaman
const totalPages = Math.ceil(totalCount / takeData);
return NextResponse.json(
{ success: true, message: "Data berhasil diambil", data: fixData },
{
success: true,
message: "Data berhasil diambil",
data: fixData,
pagination: {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
}
},
{ status: 200 }
);
} catch (error) {

View File

@@ -2,14 +2,50 @@ import { funGetDirectoryNameByValue } from "@/app_modules/_global/fun/get";
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const formData = await request.formData();
const dirId = formData.get("dirId");
const keyOfDirectory = await funGetDirectoryNameByValue({
value: dirId as string,
});
try {
const formData = await request.formData();
const dirId = formData.get("dirId");
const file = formData.get("file") as File | null;
// === LOGGING: Request Details ===
console.log("=== UPLOAD REQUEST START ===");
console.log("dirId:", dirId);
console.log("File details:");
console.log(" - Name:", file?.name);
console.log(" - Type:", file?.type);
console.log(" - Size:", file?.size, "bytes");
console.log(" - Size (KB):", file ? (file.size / 1024).toFixed(2) : "N/A");
console.log("===========================");
// FIX: Override MIME type jika salah (mobile app kadang kirim image/pdf)
let fixedFile = file;
if (file) {
const fileName = file.name.toLowerCase();
const originalType = file.type.toLowerCase();
// Jika file PDF tapi type bukan application/pdf, fix it
if (fileName.endsWith(".pdf") && originalType !== "application/pdf") {
console.log("⚠️ WARNING: PDF file has wrong MIME type:", originalType);
console.log("🔧 Overriding to: application/pdf");
// Create new File with correct MIME type
const buffer = await file.arrayBuffer();
fixedFile = new File([buffer], file.name, {
type: "application/pdf",
lastModified: file.lastModified,
});
// Rebuild formData with fixed file
formData.set("file", fixedFile);
}
}
const keyOfDirectory = await funGetDirectoryNameByValue({
value: dirId as string,
});
console.log("Directory key:", keyOfDirectory);
const res = await fetch("https://wibu-storage.wibudev.com/api/upload", {
method: "POST",
body: formData,
@@ -18,9 +54,70 @@ export async function POST(request: Request) {
},
});
const dataRes = await res.json();
// === LOGGING: Response Details ===
console.log("=== EXTERNAL STORAGE RESPONSE ===");
console.log("Status:", res.status);
console.log("Status Text:", res.statusText);
console.log("Content-Type:", res.headers.get("content-type"));
console.log("=================================");
// Cek content-type sebelum parse JSON
const contentType = res.headers.get("content-type") || "";
let dataRes;
// Try parse JSON untuk semua response (beberapa server salah set content-type)
try {
const rawResponse = await res.text();
// Coba parse sebagai JSON
try {
dataRes = JSON.parse(rawResponse);
console.log("✅ Successfully parsed response as JSON");
} catch {
// Bukan JSON - gunakan raw text
console.log("⚠️ Response is not JSON, using raw text");
if (res.ok) {
// Success tapi bukan JSON - return success response
return NextResponse.json(
{
success: true,
message: "Success upload file " + keyOfDirectory,
},
{ status: 200 }
);
} else {
return NextResponse.json(
{
success: false,
message: "Upload failed",
error: rawResponse.substring(0, 500),
fileDetails: {
name: file?.name,
type: file?.type,
size: file?.size,
}
},
{ status: res.status || 400 }
);
}
}
} catch (readError) {
console.log("❌ Failed to read response body");
console.log("Read error:", (readError as Error).message);
return NextResponse.json(
{
success: false,
message: "Failed to read response",
reason: (readError as Error).message,
},
{ status: 500 }
);
}
if (res.ok) {
console.log("✅ Upload SUCCESS");
return NextResponse.json(
{
success: true,
@@ -30,20 +127,34 @@ export async function POST(request: Request) {
{ status: 200 }
);
} else {
const errorText = await res.text();
console.log(`Failed upload ${keyOfDirectory}: ${errorText}`);
console.log("❌ Upload FAILED");
console.log("Response:", dataRes);
const errorMessage = dataRes.message || dataRes.error || JSON.stringify(dataRes);
return NextResponse.json(
{ success: false, message: errorText },
{ status: 400 }
{
success: false,
message: errorMessage || "Upload failed",
fileDetails: {
name: file?.name,
type: file?.type,
size: file?.size,
}
},
{ status: res.status || 400 }
);
}
} catch (error) {
console.log("Error upload >>", (error as Error).message || error);
console.log("=== CATCH ERROR ===");
console.log("Error:", (error as Error).message);
console.log("Stack:", (error as Error).stack);
console.log("===================");
return NextResponse.json(
{
success: false,
message: "Failed upload file",
reason: (error as Error).message || error,
reason: (error as Error).message || "Unknown error",
},
{ status: 500 }
);

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma"
export async function GET(
request: Request,

View File

@@ -1,3 +1,4 @@
import { prisma } from "@/lib";
import { NextResponse } from "next/server";
export async function GET(

View File

@@ -2,7 +2,10 @@ import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { sendNotificationMobileToManyUser } from "@/lib/mobile/notification/send-notification";
import { NotificationMobileBodyType, NotificationMobileTitleType } from "../../../../../types/type-mobile-notification";
import {
NotificationMobileBodyType,
NotificationMobileTitleType,
} from "../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
export { POST, GET };
@@ -72,15 +75,9 @@ async function GET(request: Request) {
const search = searchParams.get("search");
const category = searchParams.get("category");
const page = searchParams.get("page");
const takeData = 5;
const takeData = 10;
const skipData = (Number(page) - 1) * takeData;
// console.log("authorId", authorId);
// console.log("userLoginId", userLoginId);
// console.log("search", search);
// console.log("category", category);
console.log("page", page);
try {
if (category === "beranda") {
const blockUserId = await prisma.blockedUser

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, PUT };
@@ -9,6 +10,10 @@ async function GET(
{ params }: { params: { id: string; status: string } }
) {
const { id, status } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page ? page * takeData - takeData : 0;
const fixStatusName = _.startCase(status);
try {
@@ -22,6 +27,8 @@ async function GET(
name: fixStatusName,
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
title: true,

View File

@@ -6,6 +6,7 @@ import {
NotificationMobileTitleType,
} from "../../../../../../../types/type-mobile-notification";
import { routeAdminMobile, routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET, DELETE };
@@ -98,6 +99,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData
try {
let fixData;
@@ -117,6 +121,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
investasiId: id,
active: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
}

View File

@@ -1,10 +1,16 @@
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
try {
const data = await prisma.investasi_Invoice.findMany({
@@ -29,13 +35,30 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
},
},
take: takeData,
skip: skipData,
});
const totalData = await prisma.investasi_Invoice.count({
where: {
investasiId: id,
statusInvoiceId: "1",
},
});
const totalPages = Math.ceil(totalData / takeData);
return NextResponse.json({
status: 200,
success: true,
message: "Berhasil Mendapatkan Data",
data: data,
meta: {
currentPage: page,
totalData: totalData,
totalPage: totalPages,
dataPerPage: takeData,
},
});
} catch (error) {
return NextResponse.json({

View File

@@ -7,6 +7,7 @@ import {
NotificationMobileTitleType,
NotificationMobileBodyType,
} from "../../../../../../../types/type-mobile-notification";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET, PUT };
@@ -53,6 +54,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const authorId = searchParams.get("authorId");
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page ? page * takeData - takeData : 0;
console.log("[ID INVOICE]", id);
@@ -103,6 +107,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
statusInvoiceId: "1",
isActive: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
nominal: true,
@@ -129,6 +135,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
where: {
authorId: authorId,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
statusInvoiceId: true,

View File

@@ -2,6 +2,7 @@ import _ from "lodash";
import { prisma } from "@/lib";
import { NextResponse } from "next/server";
import { sendNotificationInvestmentAddNews } from "@/lib/mobile/notification/notification-add-news-investment";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET, DELETE };
@@ -88,8 +89,13 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
console.log("id", id);
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
let meta = null;
try {
if (category === "one-news") {
const data = await prisma.beritaInvestasi.findFirst({
@@ -113,7 +119,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
fixData = newData;
} else if (category === "all-news") {
fixData = await prisma.beritaInvestasi.findMany({
const newsData = await prisma.beritaInvestasi.findMany({
orderBy: {
updatedAt: "desc",
},
@@ -121,7 +127,27 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
investasiId: id,
active: true,
},
take: takeData,
skip: skipData,
});
const totalData = await prisma.beritaInvestasi.count({
where: {
investasiId: id,
active: true,
},
});
const totalPages = Math.ceil(totalData / takeData);
fixData = newsData;
meta = {
currentPage: page,
totalData: totalData,
totalPage: totalPages,
dataPerPage: takeData,
};
}
return NextResponse.json({
@@ -129,6 +155,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
success: true,
message: "Berita berhasil diambil",
data: fixData,
...(meta && { meta }),
});
} catch (error) {
console.log("[ERROR]", error);

View File

@@ -5,6 +5,7 @@ import moment from "moment";
import { sendNotificationMobileToManyUser } from "@/lib/mobile/notification/send-notification";
import { NotificationMobileBodyType } from "../../../../../types/type-mobile-notification";
import { routeAdminMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET };
@@ -73,6 +74,9 @@ async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const authorId = searchParams.get("authorId");
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page ? page * takeData - takeData : 0;
console.log("[CATEGORY]", category);
console.log("[AUTHOR ID]", authorId);
@@ -132,6 +136,8 @@ async function GET(request: Request) {
where: {
masterStatusInvestasiId: "1",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
imageId: true,
@@ -156,6 +162,8 @@ async function GET(request: Request) {
authorId: authorId,
statusInvoiceId: "1",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
investasiId: true,

View File

@@ -28,7 +28,5 @@ async function GET() {
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -1,3 +1,4 @@
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib";
import _ from "lodash";
import { NextRequest, NextResponse } from "next/server";
@@ -22,28 +23,38 @@ export async function GET(
let fixData;
try {
const data = await prisma.notifikasi.findMany({
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
orderBy: {
createdAt: "desc",
},
where: {
recipientId: id,
kategoriApp: fixCategory,
},
});
const data = await withRetry(
() =>
prisma.notifikasi.findMany({
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
orderBy: {
createdAt: "desc",
},
where: {
recipientId: id,
kategoriApp: fixCategory,
},
}),
undefined,
"getNotifications"
);
// Jika pagination digunakan, ambil juga total count untuk informasi
let totalCount;
let totalPages;
if (page) {
totalCount = await prisma.notifikasi.count({
where: {
recipientId: id,
kategoriApp: fixCategory,
},
});
totalCount = await withRetry(
() =>
prisma.notifikasi.count({
where: {
recipientId: id,
kategoriApp: fixCategory,
},
}),
undefined,
"countNotifications"
);
totalPages = Math.ceil(totalCount / takeData);
}
@@ -69,8 +80,23 @@ export async function GET(
return NextResponse.json(response);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("Error getting notifications:", error);
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
return NextResponse.json(
{ error: "Database connection error. Please try again." },
{ status: 503 }
);
}
return NextResponse.json(
{ error: (error as Error).message },
{ error: errorMsg },
{ status: 500 },
);
}

View File

@@ -1,3 +1,4 @@
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib";
import { NextRequest, NextResponse } from "next/server";
@@ -9,12 +10,24 @@ export async function GET(
console.log("User ID:", id);
try {
const data = await prisma.notifikasi.count({
where: {
recipientId: id,
isRead: false,
},
});
if (!id) {
return NextResponse.json({
success: false,
message: "User ID is required",
});
}
const data = await withRetry(
() =>
prisma.notifikasi.count({
where: {
recipientId: id,
isRead: false,
},
}),
undefined,
"countUnreadNotifications"
);
console.log("List Notification >>", data);
@@ -23,6 +36,21 @@ export async function GET(
data: data,
});
} catch (error) {
const errorMsg = error instanceof Error ? error.message : "Unknown error";
console.error("Error getting unread count:", error);
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
return NextResponse.json({
success: false,
message: "Database connection error. Please try again.",
});
}
return NextResponse.json({
success: false,
message: "Failed to get unread count",

View File

@@ -131,7 +131,5 @@ async function PUT(request: Request, { params }: { params: { id: string } }) {
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -40,7 +40,5 @@ export async function GET(request: Request) {
status: 500,
}
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -27,18 +27,18 @@ export async function GET(request: Request) {
NOT: {
Profile: null,
},
OR: [
{
MasterUserRole: {
name: "User",
},
},
{
MasterUserRole: {
name: "Admin",
},
},
],
// OR: [
// {
// MasterUserRole: {
// name: "User",
// },
// },
// {
// MasterUserRole: {
// name: "Admin",
// },
// },
// ],
},
include: {
Profile: {

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, PUT };
@@ -10,13 +11,11 @@ async function GET(
) {
try {
const { id, status } = params;
console.log("[ID]", id);
const fixStatusName = _.startCase(status);
console.log("[STATUS]", fixStatusName);
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData;
let data;
@@ -101,9 +100,7 @@ async function PUT(
) {
try {
const { id, status } = params;
console.log("[ID]", id);
const fixStatusName = _.startCase(status);
console.log("[STATUS]", fixStatusName);
const checkData = await prisma.voting.findFirst({
where: {
@@ -119,8 +116,6 @@ async function PUT(
},
});
console.log("[CHECKDATA]", checkData);
if (!checkData)
return NextResponse.json({
success: false,
@@ -155,8 +150,6 @@ async function PUT(
},
});
console.log("[UPDATE]", updateData);
return NextResponse.json({
success: true,
message: "Success update voting",

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -8,10 +9,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const authorId = searchParams.get("authorId");
const category = searchParams.get("category");
console.log("[ID]", id);
console.log("[AUTHOR ID]", authorId);
console.log("[CATEGORY]", category);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page ? page * takeData - takeData : 0;
let fixData;
@@ -53,7 +53,10 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
where: {
votingId: id,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
Voting_DaftarNamaVote: {
select: {
value: true,
@@ -75,8 +78,6 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
});
console.log("[LIST KONTRIBUTOR]", listKontributor);
fixData = listKontributor;
}

View File

@@ -4,6 +4,7 @@ import _ from "lodash";
import { sendNotificationMobileToManyUser } from "@/lib/mobile/notification/send-notification";
import { NotificationMobileBodyType } from "../../../../../types/type-mobile-notification";
import { routeAdminMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET };
@@ -87,8 +88,9 @@ async function GET(request: Request) {
const category = searchParams.get("category");
const authorId = searchParams.get("authorId");
const userLoginId = searchParams.get("userLoginId");
console.log("userLoginId >>", userLoginId);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
@@ -123,6 +125,8 @@ async function GET(request: Request) {
},
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
include: {
Voting_DaftarNamaVote: {
orderBy: {
@@ -154,6 +158,8 @@ async function GET(request: Request) {
where: {
authorId: authorId,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
include: {
Voting: {
select: {
@@ -211,6 +217,8 @@ async function GET(request: Request) {
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
include: {
Voting_DaftarNamaVote: {
orderBy: {
@@ -249,6 +257,8 @@ async function GET(request: Request) {
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
include: {
Voting_DaftarNamaVote: {
orderBy: {

View File

@@ -78,7 +78,5 @@ export async function GET(request: Request) {
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -49,14 +49,11 @@ export async function GET(
});
}
await prisma.$disconnect();
return NextResponse.json(
{ success: true, message: "Success get data news", data: fixData },
{ status: 200 }
);
} catch (error) {
await prisma.$disconnect();
backendLogger.error("Error get data news", error);
return NextResponse.json(
{

View File

@@ -36,8 +36,6 @@ export async function GET(
});
}
await prisma.$disconnect();
return NextResponse.json(
{ success: true, message: "Success get data document", data: fixData },
{ status: 200 }

View File

@@ -104,7 +104,5 @@ async function PUT(request: Request) {
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -77,7 +77,5 @@ async function POST(request: Request) {
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -1,4 +1,5 @@
import { decrypt } from "@/app/(auth)/_lib/decrypt";
import { withRetry } from "@/lib/prisma-retry";
import { prisma } from "@/lib";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
@@ -43,11 +44,16 @@ export async function GET(req: Request) {
);
}
const user = await prisma.user.findUnique({
where: {
id: decrypted.id,
},
});
const user = await withRetry(
() =>
prisma.user.findUnique({
where: {
id: decrypted.id,
},
}),
undefined,
"validateUser"
);
if (!user) {
return NextResponse.json(
@@ -76,15 +82,46 @@ export async function GET(req: Request) {
data: user,
});
} catch (error) {
console.error("Error in user validation:", error);
const errorMsg = error instanceof Error ? error.message : "Unknown error";
const errorStack = error instanceof Error ? error.stack : "No stack";
// Log detailed error for debugging
console.error("❌ [USER-VALIDATE] Error:", errorMsg);
console.error("❌ [USER-VALIDATE] Stack:", errorStack);
console.error("❌ [USER-VALIDATE] Time:", new Date().toISOString());
// Check if it's a database connection error
if (
errorMsg.includes("Prisma") ||
errorMsg.includes("database") ||
errorMsg.includes("connection")
) {
console.error(
"❌ [USER-VALIDATE] Database connection error detected!"
);
console.error(
"❌ [USER-VALIDATE] DATABASE_URL exists:",
!!process.env.DATABASE_URL
);
return NextResponse.json(
{
success: false,
message: "Database connection error. Please try again.",
error: process.env.NODE_ENV === "development" ? errorMsg : "Internal server error",
},
{ status: 503 }
);
}
return NextResponse.json(
{
success: false,
message: "Terjadi kesalahan pada server",
error:
process.env.NODE_ENV === "development" ? errorMsg : "Internal server error",
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -125,7 +125,5 @@ export async function GET(request: Request) {
status: 500,
}
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -1,10 +0,0 @@
import ClientLayout from "./v2_coba_tamplate";
import ViewV2 from "./v2_view";
export default async function Page() {
return (
<>
<ViewV2 />
</>
);
}

View File

@@ -1,127 +0,0 @@
"use client";
import { AccentColor, MainColor } from "@/app_modules/_global/color";
import {
listMenuHomeBody,
menuHomeJob,
} from "@/app_modules/home/component/list_menu_home";
import {
ActionIcon,
Box,
Group,
Image,
Paper,
SimpleGrid,
Stack,
Text
} from "@mantine/core";
import { IconUserSearch } from "@tabler/icons-react";
export function Test_Children() {
return (
<>
<Box>
<Image
height={140}
fit={"cover"}
alt="logo"
src={"/aset/home/home-hipmi-new.png"}
styles={{
imageWrapper: {
border: `2px solid ${AccentColor.blue}`,
borderRadius: "10px 10px 10px 10px",
},
image: {
borderRadius: "8px 8px 8px 8px",
},
}}
/>
{Array.from(new Array(2)).map((e, i) => (
<Stack my={"sm"} key={i}>
<SimpleGrid cols={2} spacing="md">
{listMenuHomeBody.map((e, i) => (
<Paper
key={e.id}
h={150}
bg={MainColor.darkblue}
style={{
borderRadius: "10px 10px 10px 10px",
border: `2px solid ${AccentColor.blue}`,
}}
onClick={() => {}}
>
<Stack align="center" justify="center" h={"100%"}>
<ActionIcon
size={50}
variant="transparent"
c={e.link == "" ? "gray.3" : MainColor.white}
>
{e.icon}
</ActionIcon>
<Text
c={e.link == "" ? "gray.3" : MainColor.white}
fz={"xs"}
>
{e.name}
</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
{/* Job View */}
<Paper
p={"md"}
w={"100%"}
bg={MainColor.darkblue}
style={{
borderRadius: "10px 10px 10px 10px",
border: `2px solid ${AccentColor.blue}`,
}}
>
<Stack onClick={() => {}}>
<Group>
<ActionIcon
variant="transparent"
size={40}
c={menuHomeJob.link == "" ? "gray.3" : MainColor.white}
>
{menuHomeJob.icon}
</ActionIcon>
<Text c={menuHomeJob.link == "" ? "gray.3" : MainColor.white}>
{menuHomeJob.name}
</Text>
</Group>
<SimpleGrid cols={2} spacing="md">
{Array.from({ length: 2 }).map((e, i) => (
<Stack key={i}>
<Group spacing={"xs"}>
<Stack h={"100%"} align="center" justify="flex-start">
<IconUserSearch size={20} color={MainColor.white} />
</Stack>
<Stack spacing={0} w={"60%"}>
<Text
lineClamp={1}
fz={"sm"}
c={MainColor.yellow}
fw={"bold"}
>
nama {i}
</Text>
<Text fz={"sm"} c={MainColor.white} lineClamp={2}>
judulnya {i}
</Text>
</Stack>
</Group>
</Stack>
))}
</SimpleGrid>
</Stack>
</Paper>
</Stack>
))}
</Box>
</>
);
}

View File

@@ -1,75 +0,0 @@
"use client"
import { MainColor } from "@/app_modules/_global/color";
import { listMenuHomeFooter } from "@/app_modules/home";
import {
ActionIcon,
Box,
Center,
SimpleGrid,
Stack,
Text,
} from "@mantine/core";
import { IconUserCircle } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
export default function Test_FooterHome() {
const router = useRouter();
return (
<Box
style={{
zIndex: 99,
borderRadius: "20px 20px 0px 0px",
}}
w={"100%"}
bottom={0}
h={"9vh"}
>
<SimpleGrid cols={listMenuHomeFooter.length + 1}>
{listMenuHomeFooter.map((e) => (
<Center h={"9vh"} key={e.id}>
<Stack
align="center"
spacing={0}
onClick={() => {
console.log("test")
}}
>
<ActionIcon
radius={"xl"}
c={e.link === "" ? "gray" : MainColor.white}
variant="transparent"
>
{e.icon}
</ActionIcon>
<Text
lineClamp={1}
c={e.link === "" ? "gray" : MainColor.white}
fz={12}
>
{e.name}
</Text>
</Stack>
</Center>
))}
<Center h={"9vh"}>
<Stack align="center" spacing={2}>
<ActionIcon
variant={"transparent"}
onClick={() =>
console.log("test")
}
>
<IconUserCircle color="white" />
</ActionIcon>
<Text fz={10} c={MainColor.white}>
Profile
</Text>
</Stack>
</Center>
</SimpleGrid>
</Box>
);
}

View File

@@ -1,130 +0,0 @@
"use client";
import { AccentColor, MainColor } from "@/app_modules/_global/color";
import {
Header,
Group,
ActionIcon,
Text,
Title,
Box,
Loader,
} from "@mantine/core";
import { IconArrowLeft, IconChevronLeft } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
export default function Test_LayoutHeaderTamplate({
title,
posotion,
// left button
hideButtonLeft,
iconLeft,
routerLeft,
customButtonLeft,
// right button
iconRight,
routerRight,
customButtonRight,
backgroundColor,
}: {
title: string;
posotion?: any;
// left button
hideButtonLeft?: boolean;
iconLeft?: any;
routerLeft?: any;
customButtonLeft?: React.ReactNode;
// right button
iconRight?: any;
routerRight?: any;
customButtonRight?: React.ReactNode;
backgroundColor?: string;
}) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isRightLoading, setRightLoading] = useState(false);
return (
<>
<Box
h={"8vh"}
// w={"100%"}
// pos={"sticky"}
// top={0}
// style={{
// zIndex: 10,
// }}
sx={{
borderStyle: "none",
}}
bg={backgroundColor ? backgroundColor : MainColor.darkblue}
>
<Group h={"100%"} position={posotion ? posotion : "apart"} px={"md"}>
{hideButtonLeft ? (
<ActionIcon disabled variant="transparent"></ActionIcon>
) : customButtonLeft ? (
customButtonLeft
) : (
<ActionIcon
c={MainColor.white}
variant="transparent"
radius={"xl"}
onClick={() => {
setIsLoading(true);
routerLeft === undefined
? router.back()
: router.push(routerLeft, { scroll: false });
}}
>
{/* PAKE LOADING SAAT KLIK BACK */}
{/* {isLoading ? (
<Loader color={AccentColor.yellow} size={20} />
) : iconLeft ? (
iconLeft
) : (
<IconChevronLeft />
)} */}
{/* GA PAKE LOADING SAAT KLIK BACK */}
{iconLeft ? (
iconLeft
) : (
<IconChevronLeft />
)}
</ActionIcon>
)}
<Title order={5} c={MainColor.yellow}>
{title}
</Title>
{customButtonRight ? (
customButtonRight
) : iconRight === undefined ? (
<ActionIcon disabled variant="transparent"></ActionIcon>
) : routerRight === undefined ? (
<Box>{iconRight}</Box>
) : (
<ActionIcon
c={"white"}
variant="transparent"
onClick={() => {
setRightLoading(true);
router.push(routerRight);
}}
>
{isRightLoading ? (
<Loader color={AccentColor.yellow} size={20} />
) : (
iconRight
)}
</ActionIcon>
)}
</Group>
</Box>
</>
);
}

View File

@@ -1,127 +0,0 @@
"use client";
import { AccentColor, MainColor } from "@/app_modules/_global/color";
import {
BackgroundImage,
Box,
Container,
rem,
ScrollArea,
} from "@mantine/core";
export function Test_Tamplate({
children,
header,
footer,
}: {
children: React.ReactNode;
header: React.ReactNode;
footer?: React.ReactNode;
}) {
return (
<>
<Box
w={"100%"}
h={"100%"}
style={{
backgroundColor: MainColor.black,
}}
>
<Container mih={"100vh"} p={0} size={rem(500)} bg={MainColor.green}>
{/* <BackgroundImage
src={"/aset/global/main_background.png"}
h={"100vh"}
// style={{ position: "relative" }}
> */}
<TestHeader header={header} />
<TestChildren footer={footer}>{children}</TestChildren>
<TestFooter footer={footer} />
{/* </BackgroundImage> */}
</Container>
</Box>
</>
);
}
export function TestHeader({ header }: { header: React.ReactNode }) {
return (
<>
<Box
h={"8vh"}
style={{
zIndex: 10,
alignContent: "center",
}}
w={"100%"}
pos={"sticky"}
top={0}
>
{header}
</Box>
</>
);
}
export function TestChildren({
children,
footer,
}: {
children: React.ReactNode;
footer: React.ReactNode;
}) {
return (
<>
<Box
style={{ zIndex: 0 }}
px={"md"}
h={footer ? "82vh" : "92vh"}
>
{children}
</Box>
</>
);
}
function TestFooter({ footer }: { footer: React.ReactNode }) {
return (
<>
{footer ? (
<Box
// w dihilangkan kalau relative
w={"100%"}
style={{
// position: "relative",
position: "fixed",
bottom: 0,
height: "10vh",
zIndex: 10,
borderRadius: "20px 20px 0px 0px",
borderTop: `2px solid ${AccentColor.blue}`,
borderRight: `1px solid ${AccentColor.blue}`,
borderLeft: `1px solid ${AccentColor.blue}`,
// maxWidth dihilangkan kalau relative
maxWidth: rem(500),
}}
bg={AccentColor.darkblue}
>
<Box
h={"100%"}
// maw dihilangkan kalau relative
maw={rem(500)}
style={{
borderRadius: "20px 20px 0px 0px",
width: "100%",
}}
// pos={"absolute"}
>
{footer}
</Box>
</Box>
) : (
""
)}
</>
);
}

View File

@@ -1,26 +0,0 @@
"use client";
import { Box, ScrollArea } from "@mantine/core";
export function V2_Children({
children,
height,
}: {
children: React.ReactNode;
height?: number;
}) {
return (
<>
<Box
style={{ zIndex: 0 }}
px={"md"}
h={height ? "82vh" : "92vh"}
pos={"static"}
>
{children}
{/* <ScrollArea h={"100%"} px={"md"} bg={"cyan"}>
</ScrollArea> */}
</Box>
</>
);
}

View File

@@ -1,159 +0,0 @@
"use client";
import { AccentColor, MainColor } from "@/app_modules/_global/color";
import {
ActionIcon,
BackgroundImage, // Import BackgroundImage dari Mantine
Box,
Button,
Container,
Group,
Text,
Title,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { createStyles } from "@mantine/styles";
import { IconBell, IconSearch } from "@tabler/icons-react";
import { ReactNode, useEffect, useState } from "react";
// Styling langsung didefinisikan di dalam komponen
const useStyles = createStyles((theme) => ({
pageContainer: {
display: "flex",
flexDirection: "column",
minHeight: "100dvh", // dynamic viewport height untuk mobile
width: "100%",
maxWidth: "500px", // Batasi lebar maksimum
margin: "0 auto", // Pusatkan layout
boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)", // Tambahkan shadow untuk efek mobile-like
backgroundColor: MainColor.darkblue, // Warna latar belakang fallback
[`@media (max-width: 768px)`]: {
maxWidth: "100%", // Pada layar mobile, gunakan lebar penuh
boxShadow: "none", // Hilangkan shadow pada mobile
},
},
header: {
position: "sticky",
top: 0,
width: "100%",
maxWidth: "500px", // Batasi lebar header sesuai container
margin: "0 auto", // Pusatkan header
backgroundColor: MainColor.darkblue,
boxShadow: "0 1px 3px rgba(0, 0, 0, 0.1)",
zIndex: 1000, // Pastikan z-index tinggi
transition: "all 0.3s ease",
color: MainColor.yellow,
},
scrolled: {
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
},
headerContainer: {
height: "8vh",
display: "flex",
alignItems: "center",
padding: "0 16px", // Padding untuk mobile view
[`@media (max-width: 768px)`]: {
height: "8vh",
},
borderBottom: `1px solid ${AccentColor.blue}`,
borderBottomLeftRadius: "10px",
borderBottomRightRadius: "10px",
},
content: {
flex: 1,
width: "100%",
overflowY: "auto", // Izinkan scrolling pada konten
paddingBottom: "15vh", // Sesuaikan dengan tinggi footer
},
footer: {
width: "100%",
backgroundColor: MainColor.darkblue,
borderTop: `1px solid ${AccentColor.blue}`,
height: "10vh", // Tinggi footer
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "fixed",
bottom: 0,
left: "50%", // Pusatkan footer
transform: "translateX(-50%)", // Pusatkan footer
maxWidth: "500px", // Batasi lebar footer
color: MainColor.white,
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
},
}));
interface ClientLayoutProps {
children: ReactNode;
}
export default function ClientLayout({ children }: ClientLayoutProps) {
const [scrolled, setScrolled] = useState<boolean>(false);
const { classes, cx } = useStyles();
// Effect untuk mendeteksi scroll
useEffect(() => {
function handleScroll() {
if (window.scrollY > 10) {
setScrolled(true);
} else {
setScrolled(false);
}
}
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<Box className={classes.pageContainer}>
{/* Header - tetap di atas */}
<Box
className={cx(classes.header, { [classes.scrolled]: scrolled })}
component="header"
>
<Container size="xl" className={classes.headerContainer}>
<Group position="apart" w={"100%"}>
<ActionIcon>
<IconSearch />
</ActionIcon>
<Title order={4}>Home Test</Title>
<ActionIcon>
<IconBell />
</ActionIcon>
</Group>
</Container>
</Box>
{/* Konten utama - bisa di-scroll */}
<Box className={classes.content}>
<Container>{children}</Container>
</Box>
{/* Footer - tetap di bawah */}
<Box className={classes.footer} component="footer">
<Container size="xl">
<Group position="apart" py="md">
<Text size="sm">© 2025 Nama Perusahaan</Text>
<Group spacing="xs">
<Button variant="subtle" size="xs">
Privasi
</Button>
<Button variant="subtle" size="xs">
Syarat
</Button>
</Group>
</Group>
</Container>
</Box>
</Box>
);
}

View File

@@ -1,42 +0,0 @@
"use client";
import { MainColor } from "@/app_modules/_global/color";
import { ActionIcon, Box, Group, Title } from "@mantine/core";
import { IconBell, IconChevronLeft } from "@tabler/icons-react";
export function V2_Header() {
return (
<>
<Box
h={"8vh"}
style={{
zIndex: 10,
alignContent: "center",
}}
w={"100%"}
pos={"sticky"}
top={0}
bg={MainColor.darkblue}
>
<Group h={"100%"} position={"apart"} px={"md"}>
<ActionIcon
c={MainColor.white}
variant="transparent"
radius={"xl"}
onClick={() => {}}
>
<IconChevronLeft />
</ActionIcon>
<Title order={5} c={MainColor.yellow}>
Test Tamplate
</Title>
<ActionIcon c={"white"} variant="transparent" onClick={() => {}}>
<IconBell />
</ActionIcon>
</Group>
</Box>
</>
);
}

View File

@@ -1,127 +0,0 @@
"use client";
import { AccentColor, MainColor } from "@/app_modules/_global/color";
import {
listMenuHomeBody,
menuHomeJob,
} from "@/app_modules/home/component/list_menu_home";
import {
Box,
Stack,
SimpleGrid,
Paper,
ActionIcon,
Group,
Image,
Text,
} from "@mantine/core";
import { IconUserSearch } from "@tabler/icons-react";
export function V2_HomeView() {
return (
<>
<Box>
<Image
height={140}
fit={"cover"}
alt="logo"
src={"/aset/home/home-hipmi-new.png"}
styles={{
imageWrapper: {
border: `2px solid ${AccentColor.blue}`,
borderRadius: "10px 10px 10px 10px",
},
image: {
borderRadius: "8px 8px 8px 8px",
},
}}
/>
{Array.from(new Array(2)).map((e, i) => (
<Stack my={"sm"} key={i}>
<SimpleGrid cols={2} spacing="md">
{listMenuHomeBody.map((e, i) => (
<Paper
key={e.id}
h={150}
bg={MainColor.darkblue}
style={{
borderRadius: "10px 10px 10px 10px",
border: `2px solid ${AccentColor.blue}`,
}}
onClick={() => {}}
>
<Stack align="center" justify="center" h={"100%"}>
<ActionIcon
size={50}
variant="transparent"
c={e.link == "" ? "gray.3" : MainColor.white}
>
{e.icon}
</ActionIcon>
<Text
c={e.link == "" ? "gray.3" : MainColor.white}
fz={"xs"}
>
{e.name}
</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
{/* Job View */}
<Paper
p={"md"}
w={"100%"}
bg={MainColor.darkblue}
style={{
borderRadius: "10px 10px 10px 10px",
border: `2px solid ${AccentColor.blue}`,
}}
>
<Stack onClick={() => {}}>
<Group>
<ActionIcon
variant="transparent"
size={40}
c={menuHomeJob.link == "" ? "gray.3" : MainColor.white}
>
{menuHomeJob.icon}
</ActionIcon>
<Text c={menuHomeJob.link == "" ? "gray.3" : MainColor.white}>
{menuHomeJob.name}
</Text>
</Group>
<SimpleGrid cols={2} spacing="md">
{Array.from({ length: 2 }).map((e, i) => (
<Stack key={i}>
<Group spacing={"xs"}>
<Stack h={"100%"} align="center" justify="flex-start">
<IconUserSearch size={20} color={MainColor.white} />
</Stack>
<Stack spacing={0} w={"60%"}>
<Text
lineClamp={1}
fz={"sm"}
c={MainColor.yellow}
fw={"bold"}
>
nama {i}
</Text>
<Text fz={"sm"} c={MainColor.white} lineClamp={2}>
judulnya {i}
</Text>
</Stack>
</Group>
</Stack>
))}
</SimpleGrid>
</Stack>
</Paper>
</Stack>
))}
</Box>
</>
);
}

View File

@@ -1,117 +0,0 @@
// app/page.tsx
"use client";
import {
Badge,
Box,
Button,
Card,
Group,
Image,
Paper,
Stack,
Text,
Title,
} from "@mantine/core";
import ClientLayout from "./v2_coba_tamplate";
export default function ViewV2() {
return (
<ClientLayout>
<Stack spacing="xl" c={"white"}>
<Title order={1} ta="center" my="lg" size="h2">
Selamat Datang
</Title>
<Text size="md" ta="center" mb="lg">
Aplikasi dengan layout yang dioptimalkan untuk tampilan mobile
</Text>
<Stack spacing="md">
{[...Array(5)].map((_, index) => (
<Card
opacity={0.3}
key={index}
shadow="sm"
padding="md"
radius="md"
withBorder
>
<Card.Section>
<Image
src={`/api/placeholder/400/200`}
height={160}
alt={`Produk ${index + 1}`}
/>
</Card.Section>
<Group position="apart" mt="md" mb="xs">
<Text fw={500}>Produk {index + 1}</Text>
<Badge color="blue" variant="light">
Baru
</Badge>
</Group>
<Text size="sm" color="dimmed" lineClamp={2}>
Deskripsi produk yang singkat dan padat untuk tampilan mobile.
Fokus pada informasi penting saja.
</Text>
<Button
variant="light"
color="blue"
fullWidth
mt="md"
radius="md"
>
Lihat Detail
</Button>
</Card>
))}
</Stack>
<Stack spacing="md">
{[...Array(5)].map((_, index) => (
<Box key={index} mb="xl" h="100px" bg={"gray"}>
Test
</Box>
))}
</Stack>
{[...Array(5)].map((_, index) => (
<div
key={index}
style={{
backgroundColor: "gray",
marginBottom: "15px",
height: "100px",
}}
>
Test
</div>
))}
<Paper
shadow="md"
p="md"
withBorder
radius="md"
style={{
backgroundImage: "linear-gradient(45deg, #228be6, #4c6ef5)",
color: "white",
}}
>
<Text fw={700} size="lg" ta="center">
Promo Spesial
</Text>
<Text ta="center" my="sm">
Dapatkan diskon 20% untuk pembelian pertama
</Text>
<Button variant="white" color="blue" fullWidth>
Klaim Sekarang
</Button>
</Paper>
</Stack>
</ClientLayout>
);
}

View File

@@ -1,60 +0,0 @@
"use client";
import { MainColor } from "@/app_modules/_global/color";
import {
Avatar,
Button,
Center,
FileButton,
Paper,
Stack,
} from "@mantine/core";
import { IconCamera } from "@tabler/icons-react";
import { useState } from "react";
import { DIRECTORY_ID } from "../../lib";
export default function Page() {
const [data, setData] = useState({
name: "bagas",
hobi: [
{
id: "1",
name: "mancing",
},
{
id: "2",
name: "game",
},
],
});
return (
<>
<Stack align="center" justify="center" h={"100vh"}>
<pre>{JSON.stringify(data, null, 2)}</pre>
<Button
onClick={() => {
const newData = [
{
id: "1",
name: "sepedah",
},
{
id: "2",
name: "berenang",
},
];
setData({
...data,
hobi: newData,
});
}}
>
Ganti
</Button>
</Stack>
</>
);
}

View File

@@ -1,46 +0,0 @@
"use client";
import { Button } from "@mantine/core";
interface DownloadButtonProps {
fileUrl: string;
fileName: string;
}
export default function Coba() {
const fileUrl =
"https://wibu-storage.wibudev.com/api/pdf-to-image?url=https://wibu-storage.wibudev.com/api/files/cm7liew81000t3y8ax1v6yo02";
const fileName = "example.pdf"; // Nama file yang akan diunduh
return (
<div>
<h1>Download File Example</h1>
<DownloadButton fileUrl={fileUrl} fileName={fileName} />
</div>
);
}
export function DownloadButton({ fileUrl, fileName }: DownloadButtonProps) {
const handleDownloadFromAPI = async () => {
try {
const response = await fetch("https://wibu-storage.wibudev.com/api/files/cm7liew81000t3y8ax1v6yo02")
const blob = await response.blob(); // Konversi respons ke Blob
const url = window.URL.createObjectURL(blob); // Buat URL untuk Blob
const link = document.createElement("a");
link.href = url;
link.download = "generated-file.pdf"; // Nama file yang akan diunduh
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url); // Bersihkan URL
} catch (error) {
console.error("Error downloading file:", error);
}
};
return (
<Button onClick={handleDownloadFromAPI} variant="outline" color="blue">
Download File
</Button>
);
}

View File

@@ -1,13 +0,0 @@
import Coba from "./_view";
async function Page() {
return (
<>
<Coba />
</>
);
}
export default Page;

View File

@@ -1,79 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from "react";
// Tipe untuk data item (sesuaikan sesuai API kamu)
interface Item {
id: number;
name: string;
}
// Props komponen
interface InfiniteScrollProps<T> {
fetchFunction: (page: number) => Promise<T[]>;
renderItem: (item: T) => React.ReactNode;
itemsPerPage?: number;
threshold?: number; // Jarak dari bawah halaman untuk memicu load
}
const InfiniteScroll = <T,>({
fetchFunction,
renderItem,
itemsPerPage = 10,
threshold = 50,
}: InfiniteScrollProps<T>) => {
const [items, setItems] = useState<T[]>([]);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
// Load data awal
useEffect(() => {
const loadInitialData = async () => {
const data = await fetchFunction(page);
if (data.length === 0) setHasMore(false);
setItems(data);
};
loadInitialData();
}, [fetchFunction, page]);
// Handle scroll event
useEffect(() => {
const handleScroll = () => {
const isBottom =
window.innerHeight + window.scrollY >=
document.body.offsetHeight - threshold;
if (isBottom && hasMore) {
loadMoreItems();
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [hasMore, threshold]);
const loadMoreItems = async () => {
const nextPage = page + 1;
const newItems = await fetchFunction(nextPage);
if (newItems.length === 0) {
setHasMore(false);
}
setItems((prev) => [...prev, ...newItems]);
setPage(nextPage);
};
return (
<div >
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
{!hasMore && <p>🎉 Semua data telah dimuat.</p>}
</div>
);
};
export default InfiniteScroll;

View File

@@ -1,46 +0,0 @@
"use client"
import React, { useState } from "react";
import InfiniteScroll from "./_comp/ui_scroll";
import { apiGetMessageByRoomId } from "@/app_modules/colab/_lib/api_collaboration";
import { ChatMessage } from "@/app/dev/(user)/colab/_comp/interface";
// Definisikan tipe data
interface User {
id: number;
name: string;
email: string;
}
// Komponen App
function App() {
const [data, setData] = useState<ChatMessage[]>([]);
// Simulasi API call
const fetchUsers = async (page: number): Promise<ChatMessage[]> => {
const response = await apiGetMessageByRoomId({
id: "cmb5x31dt0001tl7y7vj26pfy",
});
setData(response.data);
return response.data;
};
return (
<div style={{ padding: "20px" }}>
<h1>Infinite Scroll with TypeScript</h1>
<InfiniteScroll<ChatMessage>
fetchFunction={fetchUsers}
itemsPerPage={10}
threshold={100}
renderItem={(item) => (
<div style={{ marginBottom: "10px" }}>
<strong>{item.User?.Profile?.name}</strong> - {item.message}
</div>
)}
/>
</div>
);
}
export default App;

View File

@@ -1,56 +0,0 @@
"use client";
import { ComponentGlobal_CardStyles } from "@/app_modules/_global/component";
import {
UIGlobal_LayoutHeaderTamplate,
UIGlobal_LayoutTamplate,
} from "@/app_modules/_global/ui";
import CustomSkeleton from "@/app_modules/components/CustomSkeleton";
import { Button, Center, Grid, Group, Skeleton, Stack } from "@mantine/core";
import Link from "next/link";
export default function Voting_ComponentSkeletonViewPuh() {
return (
<>
<UIGlobal_LayoutTamplate
header={<UIGlobal_LayoutHeaderTamplate title="Skeleton Maker" />}
>
<Stack>
<CustomSkeleton height={300} width={"100%"} />
<Center>
<CustomSkeleton height={40} radius={"xl"} width={"50%"} />
</Center>
<CustomSkeleton height={500} width={"100%"} />
<CustomSkeleton height={40} radius={"xl"} width={"100%"} />
</Stack>
{/* <Grid align="center">
<Grid.Col span={2}>
<CustomSkeleton height={40} width={40} circle />
</Grid.Col>
<Grid.Col span={4}>
<CustomSkeleton height={20} width={"100%"} />
</Grid.Col>
<Grid.Col span={3} offset={3}>
<Group position="right">
<CustomSkeleton height={20} width={"50%"} />
</Group>
</Grid.Col>
</Grid>
<Stack>
<CustomSkeleton height={20} width={"100%"} radius={"xl"} />
<CustomSkeleton height={20} width={"100%"} radius={"xl"} />
</Stack> */}
{/* <Stack spacing={"xl"} p={"sm"}>
{Array.from({ length: 4 }).map((_, i) => (
<CustomSkeleton key={i} height={50} width={"100%"} />
))}
<CustomSkeleton height={100} width={"100%"} />
<CustomSkeleton radius="xl" height={50} width={"100%"} />
</Stack> */}
</UIGlobal_LayoutTamplate>
</>
);
}

View File

@@ -1,47 +0,0 @@
"use client";
import { gs_realtimeData, IRealtimeData } from "@/lib/global_state";
import { Button, Stack } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { useAtom } from "jotai";
import { WibuRealtime } from "wibu-pkg";
import { v4 } from "uuid";
export default function Page() {
const [dataRealtime, setDataRealtime] = useAtom(gs_realtimeData);
useShallowEffect(() => {
console.log(
dataRealtime?.userId == "user2"
? console.log("")
: console.log(dataRealtime)
);
}, [dataRealtime]);
async function onSend() {
const newData: IRealtimeData = {
appId: v4(),
status: "Publish",
userId: "user2",
pesan: "apa kabar",
title: "coba",
kategoriApp: "INVESTASI",
};
WibuRealtime.setData({
type: "notification",
pushNotificationTo: "ADMIN",
});
}
return (
<Stack p={"md"} align="center" justify="center" h={"80vh"}>
<Button
onClick={() => {
onSend();
}}
>
Dari test 2 cuma notif
</Button>
</Stack>
);
}

View File

@@ -1,140 +0,0 @@
"use client";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import { useState } from "react";
import {
Box,
Button,
Group,
Image as MantineImage,
Stack,
Text,
} from "@mantine/core";
import Underline from "@tiptap/extension-underline";
import { MainColor } from "@/app_modules/_global/color";
const listStiker = [
{
id: 2,
name: "stiker2",
url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQN9AKmsBY4yqdn3GueJJEVPJbfmf853gDL4cN8uc9eqsCTiJ1fzhcpywzVP68NCJEA5NQ&usqp=CAU",
},
{
id: 3,
name: "stiker3",
url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS2lkV3ZiQ8m-OELSui2JGVy80vnh1cyRUV7NrgFNluPVVs2HUAyCHwCMAKGe2s5jk2sn8&usqp=CAU",
},
{
id: 4,
name: "stiker4",
url: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQHy9ZdsPc6dHgVTl5yIGpRJ-KtpTIsXA2_kbfO1Oc-pv_f7CNKGxhO56RjKujE3xCyb9k&usqp=CAU",
},
];
export default function RichTextWithStickers() {
const [chat, setChat] = useState<string[]>([]);
const editor = useEditor({
extensions: [
StarterKit, // Sudah include Bold, Italic, dll
Underline, // Tambahan untuk underline
Image,
],
content: "",
});
const insertSticker = (url: string) => {
editor?.chain().focus().setImage({ src: url }).run();
};
return (
<Stack p="md">
<Text fw={700}>Tiptap Editor dengan Stiker Inline</Text>
<Box
style={{
border: "1px solid #ccc",
borderRadius: 4,
padding: 8,
minHeight: 150,
backgroundColor: MainColor.white,
}}
>
<Group spacing="xs" mb="sm">
<Button
variant="default"
onClick={() => editor?.chain().focus().toggleBold().run()}
>
B
</Button>
<Button
variant="default"
onClick={() => editor?.chain().focus().toggleItalic().run()}
>
I
</Button>
<Button
variant="default"
onClick={() => editor?.chain().focus().toggleUnderline().run()}
>
U
</Button>
</Group>
<EditorContent
editor={editor}
style={{
backgroundColor: "white",
}}
/>
</Box>
<Button
mt="sm"
onClick={() => {
if (editor) {
setChat((prev) => [...prev, editor.getHTML()]);
editor.commands.clearContent();
}
}}
>
Kirim
</Button>
<Group>
{listStiker.map((item) => (
<Box
key={item.id}
component="button"
onClick={() => insertSticker(item.url)}
style={{
border: "none",
background: "transparent",
cursor: "pointer",
}}
>
<MantineImage
w={30}
h={30}
src={item.url}
alt={item.name}
styles={{
image: {
width: 30,
height: 30,
},
}}
/>
</Box>
))}
</Group>
{/* <Stack mt="lg" p="md" bg="gray.1">
{chat.map((item, index) => (
<Box key={index} dangerouslySetInnerHTML={{ __html: item }} />
))}
</Stack> */}
</Stack>
);
}

View File

@@ -1,210 +0,0 @@
"use client";
import React, { useState, useEffect } from "react";
import {
Box,
Button,
Group,
Image,
Paper,
ScrollArea,
SimpleGrid,
Stack,
Text,
Tooltip,
Modal,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import dynamic from "next/dynamic";
import { MainColor } from "@/app_modules/_global/color";
import { listStiker } from "@/app_modules/_global/lib/stiker";
// Dynamic import ReactQuill dengan SSR disabled
const ReactQuill = dynamic(
async () => {
const { default: RQ } = await import("react-quill");
// Tidak perlu import CSS dengan import statement
return function comp({ forwardedRef, ...props }: any) {
return <RQ ref={forwardedRef} {...props} />;
};
},
{ ssr: false, loading: () => <p>Loading Editor...</p> }
);
type ChatItem = {
content: string; // HTML content including text and stickers
};
export default function Page() {
const [editorContent, setEditorContent] = useState("");
const [chat, setChat] = useState<ChatItem[]>([]);
const [opened, { open, close }] = useDisclosure(false);
const quillRef = React.useRef<any>(null);
const [quillLoaded, setQuillLoaded] = useState(false);
// Load CSS on client-side only
useEffect(() => {
// Add Quill CSS via <link> tag
const link = document.createElement("link");
link.href = "https://cdn.quilljs.com/1.3.6/quill.snow.css";
link.rel = "stylesheet";
document.head.appendChild(link);
// Add custom style for stickers inside Quill editor
const style = document.createElement("style");
style.textContent = `
.ql-editor img {
max-width: 100px !important;
max-height: 100px !important;
}
.chat-content img {
max-width: 70px !important;
max-height: 70px !important;
}
`;
document.head.appendChild(style);
setQuillLoaded(true);
return () => {
// Clean up when component unmounts
document.head.removeChild(link);
document.head.removeChild(style);
};
}, []);
// Custom toolbar options for ReactQuill
const modules = {
toolbar: [
[{ header: [1, 2, false] }],
["bold", "italic", "underline", "strike", "blockquote"],
[{ list: "ordered" }, { list: "bullet" }],
["link", "image"],
["clean"],
],
};
const formats = [
"header",
"bold",
"italic",
"underline",
"strike",
"blockquote",
"list",
"bullet",
"link",
"image",
];
const insertSticker = (stickerUrl: string) => {
if (!quillRef.current) return;
const quill = quillRef.current.getEditor();
const range = quill.getSelection(true);
// Custom image insertion with size
// Use custom blot or HTML string with size attributes
const stickerHtml = `<img src="${stickerUrl}" alt="sticker" style="width: 40px; height: 40px;">`;
// Insert HTML at cursor position
quill.clipboard.dangerouslyPasteHTML(range.index, stickerHtml);
// Move cursor after inserted sticker
quill.setSelection(range.index + 1, 0);
// Focus back on editor
quill.focus();
// Close sticker modal
close();
};
// Function to send message
const sendMessage = () => {
if (editorContent.trim() !== "") {
setChat((prev) => [...prev, { content: editorContent }]);
setEditorContent(""); // Clear after sending
}
};
return (
<Stack p={"md"} spacing="md">
<SimpleGrid cols={2}>
<Stack bg={"gray.1"} h={560} p="md">
<Stack>
<ScrollArea>
{chat.map((item, index) => (
<Box key={index} mb="md">
<div
className="chat-content"
dangerouslySetInnerHTML={{ __html: item.content }}
/>
</Box>
))}
</ScrollArea>
</Stack>
</Stack>
<Paper withBorder p="md">
<Text size="sm" weight={500} mb="xs">
Chat Preview Data:
</Text>
<ScrollArea h={520}>
<pre style={{ whiteSpace: "pre-wrap" }}>
{JSON.stringify(chat, null, 2)}
</pre>
</ScrollArea>
</Paper>
</SimpleGrid>
<Box w="100%" maw={800}>
<Box mb="xs" bg={MainColor.white}>
{quillLoaded && (
<ReactQuill
forwardedRef={quillRef}
theme="snow"
value={editorContent}
onChange={setEditorContent}
modules={modules}
formats={formats}
placeholder="Ketik pesan di sini atau tambahkan stiker..."
style={{
height: 120,
marginBottom: 40,
backgroundColor: MainColor.white,
}}
/>
)}
</Box>
<Group position="apart">
<Button variant="outline" onClick={open} color="blue">
Tambah Stiker
</Button>
<Button onClick={sendMessage}>Kirim Pesan</Button>
</Group>
</Box>
{/* Sticker Modal */}
<Modal opened={opened} onClose={close} title="Pilih Stiker" size="md">
<SimpleGrid cols={3} spacing="md">
{listStiker.map((item) => (
<Box key={item.id}>
<Tooltip label={item.name}>
<Image
src={item.url}
height={100}
width={100}
alt={item.name}
style={{ cursor: "pointer" }}
onClick={() => insertSticker(item.url)}
/>
</Tooltip>
</Box>
))}
</SimpleGrid>
</Modal>
</Stack>
);
}

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