diff --git a/CHANGELOG.md b/CHANGELOG.md
index 90c325da..2d53cb3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,30 @@
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)
diff --git a/DEBUG_UPLOAD_FILE.md b/DEBUG_UPLOAD_FILE.md
new file mode 100644
index 00000000..5fda9054
--- /dev/null
+++ b/DEBUG_UPLOAD_FILE.md
@@ -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:
413 Request Entity Too Large
+=================================
+```
+
+## ๐ง 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
diff --git a/Dockerfile b/Dockerfile
index 1207f778..4d7a6742 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,10 @@
# ==============================
# Stage 1: Builder
# ==============================
-FROM node:20-bookworm-slim AS builder
+FROM oven/bun:1-debian AS builder
WORKDIR /app
-# Install system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
libc6 \
git \
@@ -13,39 +12,32 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
-# Copy dependency files first (for better caching)
-COPY package.json package-lock.json* bun.lockb* ./
+COPY package.json bun.lockb* ./
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_OPTIONS="--max-old-space-size=4096"
-# ๐ฅ Skip postinstall scripts (fix onnxruntime error)
-RUN npm install --legacy-peer-deps --ignore-scripts
-
-# Copy full source
+RUN bun install --frozen-lockfile
COPY . .
-# Use .env.example as build env
-# (Pastikan file ini ada di project)
RUN cp .env.example .env || true
-# Generate Prisma Client
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
-RUN npx prisma generate
+RUN bunx prisma generate
-# Build Next.js
-RUN npm run build
+RUN bun run build
# ==============================
# Stage 2: Runner (Production)
# ==============================
-FROM node:20-bookworm-slim AS runner
+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 \
@@ -55,10 +47,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
RUN groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs nextjs
-# Copy standalone output
-COPY --from=builder /app/.next/standalone ./
-COPY --from=builder /app/.next/static ./.next/static
+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
@@ -69,4 +65,4 @@ EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
-CMD ["node", "server.js"]
\ No newline at end of file
+CMD ["bun", "start"]
\ No newline at end of file
diff --git a/next.config.js b/next.config.js
index bd659ae2..43974747 100644
--- a/next.config.js
+++ b/next.config.js
@@ -16,6 +16,21 @@ const nextConfig = {
}
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",
+ },
+ ],
+ },
+ ];
+ },
};
-module.exports = nextConfig;
\ No newline at end of file
+module.exports = nextConfig;
diff --git a/package.json b/package.json
index df2b869b..f3c2c87b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "hipmi",
- "version": "1.6.8",
+ "version": "1.7.5",
"private": true,
"prisma": {
"seed": "bun prisma/seed.ts"
diff --git a/public/.well-known/assetlinks.json b/public/.well-known/assetlinks.json
index 7aceb970..592ad578 100644
--- a/public/.well-known/assetlinks.json
+++ b/public/.well-known/assetlinks.json
@@ -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"]
+ }
}
-}]
\ No newline at end of file
+]
\ No newline at end of file
diff --git a/src/app/(event-confirmation)/event/[id]/confirmation/route.ts b/src/app/(event-confirmation)/event/[id]/confirmation/route.ts
new file mode 100644
index 00000000..08e3a739
--- /dev/null
+++ b/src/app/(event-confirmation)/event/[id]/confirmation/route.ts
@@ -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",
+ },
+ },
+ );
+}
diff --git a/src/app/(support)/support-center/page.tsx b/src/app/(support)/support-center/page.tsx
index 9b23a078..9aefdc16 100644
--- a/src/app/(support)/support-center/page.tsx
+++ b/src/app/(support)/support-center/page.tsx
@@ -98,7 +98,7 @@ export default function SupportCenter() {
Support Center
- Send us a message and we'll get back to you as soon as possible.
+ Send us a message and we'll get back to you as soon as possible.
diff --git a/src/app/api/auth/mobile-validasi/route.ts b/src/app/api/auth/mobile-validasi/route.ts
index f0d94e08..6a8e42a1 100644
--- a/src/app/api/auth/mobile-validasi/route.ts
+++ b/src/app/api/auth/mobile-validasi/route.ts
@@ -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,
diff --git a/src/app/api/mobile/file/route.ts b/src/app/api/mobile/file/route.ts
index b3809d93..d8092876 100644
--- a/src/app/api/mobile/file/route.ts
+++ b/src/app/api/mobile/file/route.ts
@@ -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 }
);
diff --git a/src/app/api/mobile/forum/[id]/preview-report-posting/route.ts b/src/app/api/mobile/forum/[id]/preview-report-posting/route.ts
index b68db0eb..ea671197 100644
--- a/src/app/api/mobile/forum/[id]/preview-report-posting/route.ts
+++ b/src/app/api/mobile/forum/[id]/preview-report-posting/route.ts
@@ -1,3 +1,4 @@
+import { prisma } from "@/lib";
import { NextResponse } from "next/server";
export async function GET(
diff --git a/src/app/api/mobile/forum/route.ts b/src/app/api/mobile/forum/route.ts
index 9ecc0f91..45d66cad 100644
--- a/src/app/api/mobile/forum/route.ts
+++ b/src/app/api/mobile/forum/route.ts
@@ -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
diff --git a/src/app/zCoba/home/page.tsx b/src/app/zCoba/home/page.tsx
deleted file mode 100644
index 437474b1..00000000
--- a/src/app/zCoba/home/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import ClientLayout from "./v2_coba_tamplate";
-import ViewV2 from "./v2_view";
-
-export default async function Page() {
- return (
- <>
-
- >
- );
-}
diff --git a/src/app/zCoba/home/test_children.tsx b/src/app/zCoba/home/test_children.tsx
deleted file mode 100644
index e3a96278..00000000
--- a/src/app/zCoba/home/test_children.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
-
- {Array.from(new Array(2)).map((e, i) => (
-
-
- {listMenuHomeBody.map((e, i) => (
- {}}
- >
-
-
- {e.icon}
-
-
- {e.name}
-
-
-
- ))}
-
-
- {/* Job View */}
-
- {}}>
-
-
- {menuHomeJob.icon}
-
-
- {menuHomeJob.name}
-
-
-
- {Array.from({ length: 2 }).map((e, i) => (
-
-
-
-
-
-
-
- nama {i}
-
-
- judulnya {i}
-
-
-
-
- ))}
-
-
-
-
- ))}
-
- >
- );
-}
diff --git a/src/app/zCoba/home/test_footer.tsx b/src/app/zCoba/home/test_footer.tsx
deleted file mode 100644
index baa2368a..00000000
--- a/src/app/zCoba/home/test_footer.tsx
+++ /dev/null
@@ -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 (
-
-
- {listMenuHomeFooter.map((e) => (
-
- {
- console.log("test")
- }}
- >
-
- {e.icon}
-
-
- {e.name}
-
-
-
- ))}
-
-
-
-
- console.log("test")
- }
- >
-
-
-
- Profile
-
-
-
-
-
- );
-}
diff --git a/src/app/zCoba/home/test_header.tsx b/src/app/zCoba/home/test_header.tsx
deleted file mode 100644
index df65017a..00000000
--- a/src/app/zCoba/home/test_header.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
- {hideButtonLeft ? (
-
- ) : customButtonLeft ? (
- customButtonLeft
- ) : (
- {
- setIsLoading(true);
- routerLeft === undefined
- ? router.back()
- : router.push(routerLeft, { scroll: false });
- }}
- >
- {/* PAKE LOADING SAAT KLIK BACK */}
- {/* {isLoading ? (
-
- ) : iconLeft ? (
- iconLeft
- ) : (
-
- )} */}
-
-
- {/* GA PAKE LOADING SAAT KLIK BACK */}
- {iconLeft ? (
- iconLeft
- ) : (
-
- )}
-
- )}
-
-
- {title}
-
-
- {customButtonRight ? (
- customButtonRight
- ) : iconRight === undefined ? (
-
- ) : routerRight === undefined ? (
- {iconRight}
- ) : (
- {
- setRightLoading(true);
- router.push(routerRight);
- }}
- >
- {isRightLoading ? (
-
- ) : (
- iconRight
- )}
-
- )}
-
-
- >
- );
-}
diff --git a/src/app/zCoba/home/test_tamplate.tsx b/src/app/zCoba/home/test_tamplate.tsx
deleted file mode 100644
index ff55f50a..00000000
--- a/src/app/zCoba/home/test_tamplate.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
- {/* */}
-
-
- {children}
-
-
- {/* */}
-
-
- >
- );
-}
-
-export function TestHeader({ header }: { header: React.ReactNode }) {
- return (
- <>
-
- {header}
-
- >
- );
-}
-
-export function TestChildren({
- children,
- footer,
-}: {
- children: React.ReactNode;
- footer: React.ReactNode;
-}) {
- return (
- <>
-
- {children}
-
- >
- );
-}
-
-function TestFooter({ footer }: { footer: React.ReactNode }) {
- return (
- <>
- {footer ? (
-
-
- {footer}
-
-
- ) : (
- ""
- )}
- >
- );
-}
diff --git a/src/app/zCoba/home/v2_children.tsx b/src/app/zCoba/home/v2_children.tsx
deleted file mode 100644
index 7942ba69..00000000
--- a/src/app/zCoba/home/v2_children.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-"use client";
-
-import { Box, ScrollArea } from "@mantine/core";
-
-export function V2_Children({
- children,
- height,
-}: {
- children: React.ReactNode;
- height?: number;
-}) {
- return (
- <>
-
- {children}
- {/*
- */}
-
- >
- );
-}
diff --git a/src/app/zCoba/home/v2_coba_tamplate.tsx b/src/app/zCoba/home/v2_coba_tamplate.tsx
deleted file mode 100644
index 58fec604..00000000
--- a/src/app/zCoba/home/v2_coba_tamplate.tsx
+++ /dev/null
@@ -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(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 (
-
- {/* Header - tetap di atas */}
-
-
-
-
-
-
- Home Test
-
-
-
-
-
-
-
- {/* Konten utama - bisa di-scroll */}
-
- {children}
-
-
- {/* Footer - tetap di bawah */}
-
-
-
- ยฉ 2025 Nama Perusahaan
-
-
-
-
-
-
-
-
- );
-}
diff --git a/src/app/zCoba/home/v2_header.tsx b/src/app/zCoba/home/v2_header.tsx
deleted file mode 100644
index 38a72361..00000000
--- a/src/app/zCoba/home/v2_header.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
- {}}
- >
-
-
-
-
- Test Tamplate
-
-
- {}}>
-
-
-
-
- >
- );
-}
diff --git a/src/app/zCoba/home/v2_home_view.tsx b/src/app/zCoba/home/v2_home_view.tsx
deleted file mode 100644
index 90e0fc9a..00000000
--- a/src/app/zCoba/home/v2_home_view.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
-
- {Array.from(new Array(2)).map((e, i) => (
-
-
- {listMenuHomeBody.map((e, i) => (
- {}}
- >
-
-
- {e.icon}
-
-
- {e.name}
-
-
-
- ))}
-
-
- {/* Job View */}
-
- {}}>
-
-
- {menuHomeJob.icon}
-
-
- {menuHomeJob.name}
-
-
-
- {Array.from({ length: 2 }).map((e, i) => (
-
-
-
-
-
-
-
- nama {i}
-
-
- judulnya {i}
-
-
-
-
- ))}
-
-
-
-
- ))}
-
- >
- );
-}
diff --git a/src/app/zCoba/home/v2_view.tsx b/src/app/zCoba/home/v2_view.tsx
deleted file mode 100644
index 34b8933f..00000000
--- a/src/app/zCoba/home/v2_view.tsx
+++ /dev/null
@@ -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 (
-
-
-
- Selamat Datang
-
-
-
- Aplikasi dengan layout yang dioptimalkan untuk tampilan mobile
-
-
-
- {[...Array(5)].map((_, index) => (
-
-
-
-
-
-
- Produk {index + 1}
-
- Baru
-
-
-
-
- Deskripsi produk yang singkat dan padat untuk tampilan mobile.
- Fokus pada informasi penting saja.
-
-
-
-
- ))}
-
-
-
- {[...Array(5)].map((_, index) => (
-
- Test
-
- ))}
-
-
- {[...Array(5)].map((_, index) => (
-
- Test
-
- ))}
-
-
-
- Promo Spesial
-
-
- Dapatkan diskon 20% untuk pembelian pertama
-
-
-
-
-
- );
-}
diff --git a/src/app/zCoba/page.tsx b/src/app/zCoba/page.tsx
deleted file mode 100644
index 8c65e79c..00000000
--- a/src/app/zCoba/page.tsx
+++ /dev/null
@@ -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 (
- <>
-
- {JSON.stringify(data, null, 2)}
-
-
-
- >
- );
-}
diff --git a/src/app/zCoba/pdf/_view.tsx b/src/app/zCoba/pdf/_view.tsx
deleted file mode 100644
index fc7378ef..00000000
--- a/src/app/zCoba/pdf/_view.tsx
+++ /dev/null
@@ -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 (
-
-
Download File Example
-
-
- );
-}
-
-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 (
-
- );
-}
diff --git a/src/app/zCoba/pdf/page.tsx b/src/app/zCoba/pdf/page.tsx
deleted file mode 100644
index a645fa5d..00000000
--- a/src/app/zCoba/pdf/page.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import Coba from "./_view";
-
-
-
-async function Page() {
- return (
- <>
-
- >
- );
-}
-
-export default Page;
diff --git a/src/app/zCoba/scroll/_comp/ui_scroll.tsx b/src/app/zCoba/scroll/_comp/ui_scroll.tsx
deleted file mode 100644
index 4d0651f7..00000000
--- a/src/app/zCoba/scroll/_comp/ui_scroll.tsx
+++ /dev/null
@@ -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 {
- fetchFunction: (page: number) => Promise;
- renderItem: (item: T) => React.ReactNode;
- itemsPerPage?: number;
- threshold?: number; // Jarak dari bawah halaman untuk memicu load
-}
-
-const InfiniteScroll = ({
- fetchFunction,
- renderItem,
- itemsPerPage = 10,
- threshold = 50,
-}: InfiniteScrollProps) => {
- const [items, setItems] = useState([]);
- 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 (
-
-
- {items.map((item, index) => (
- - {renderItem(item)}
- ))}
-
- {!hasMore &&
๐ Semua data telah dimuat.
}
-
- );
-};
-
-export default InfiniteScroll;
diff --git a/src/app/zCoba/scroll/page.tsx b/src/app/zCoba/scroll/page.tsx
deleted file mode 100644
index 8adcf1d7..00000000
--- a/src/app/zCoba/scroll/page.tsx
+++ /dev/null
@@ -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([]);
- // Simulasi API call
- const fetchUsers = async (page: number): Promise => {
- const response = await apiGetMessageByRoomId({
- id: "cmb5x31dt0001tl7y7vj26pfy",
- });
- setData(response.data);
- return response.data;
- };
-
- return (
-
-
Infinite Scroll with TypeScript
-
- fetchFunction={fetchUsers}
- itemsPerPage={10}
- threshold={100}
- renderItem={(item) => (
-
- {item.User?.Profile?.name} - {item.message}
-
- )}
- />
-
- );
-}
-
-export default App;
diff --git a/src/app/zCoba/skeleton/page.tsx b/src/app/zCoba/skeleton/page.tsx
deleted file mode 100644
index df2e7133..00000000
--- a/src/app/zCoba/skeleton/page.tsx
+++ /dev/null
@@ -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 (
- <>
- }
- >
-
-
-
-
-
-
-
-
-
- {/*
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- */}
-
- {/*
- {Array.from({ length: 4 }).map((_, i) => (
-
- ))}
-
-
- */}
-
- >
- );
-}
diff --git a/src/app/zCoba/test2/page.tsx b/src/app/zCoba/test2/page.tsx
deleted file mode 100644
index d010f19e..00000000
--- a/src/app/zCoba/test2/page.tsx
+++ /dev/null
@@ -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 (
-
-
-
- );
-}
diff --git a/src/app/zCoba/text_editor/page.tsx b/src/app/zCoba/text_editor/page.tsx
deleted file mode 100644
index 76fc18ac..00000000
--- a/src/app/zCoba/text_editor/page.tsx
+++ /dev/null
@@ -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([]);
-
- 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 (
-
- Tiptap Editor dengan Stiker Inline
-
-
-
-
-
-
-
-
-
-
-
-
-
- {listStiker.map((item) => (
- insertSticker(item.url)}
- style={{
- border: "none",
- background: "transparent",
- cursor: "pointer",
- }}
- >
-
-
- ))}
-
-
- {/*
- {chat.map((item, index) => (
-
- ))}
- */}
-
- );
-}
diff --git a/src/app/zCoba/text_editor2/page.tsx b/src/app/zCoba/text_editor2/page.tsx
deleted file mode 100644
index 580423c3..00000000
--- a/src/app/zCoba/text_editor2/page.tsx
+++ /dev/null
@@ -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 ;
- };
- },
- { ssr: false, loading: () => Loading Editor...
}
-);
-
-
-type ChatItem = {
- content: string; // HTML content including text and stickers
-};
-
-export default function Page() {
- const [editorContent, setEditorContent] = useState("");
- const [chat, setChat] = useState([]);
- const [opened, { open, close }] = useDisclosure(false);
- const quillRef = React.useRef(null);
- const [quillLoaded, setQuillLoaded] = useState(false);
-
- // Load CSS on client-side only
- useEffect(() => {
- // Add Quill CSS via 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 = `
`;
-
- // 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 (
-
-
-
-
-
- {chat.map((item, index) => (
-
-
-
- ))}
-
-
-
-
-
- Chat Preview Data:
-
-
-
- {JSON.stringify(chat, null, 2)}
-
-
-
-
-
-
-
- {quillLoaded && (
-
- )}
-
-
-
-
-
-
-
-
-
- {/* Sticker Modal */}
-
-
- {listStiker.map((item) => (
-
-
- insertSticker(item.url)}
- />
-
-
- ))}
-
-
-
- );
-}
diff --git a/src/app/zCoba/upload/page.tsx b/src/app/zCoba/upload/page.tsx
deleted file mode 100644
index 34ee4314..00000000
--- a/src/app/zCoba/upload/page.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-"use client";
-
-import { MainColor } from "@/app_modules/_global/color";
-import { ComponentGlobal_BoxUploadImage } from "@/app_modules/_global/component";
-import { funGlobal_UploadToStorage } from "@/app_modules/_global/fun";
-import {
- UIGlobal_LayoutHeaderTamplate,
- UIGlobal_LayoutTamplate,
-} from "@/app_modules/_global/ui";
-import { clientLogger } from "@/util/clientLogger";
-import {
- AspectRatio,
- Button,
- Center,
- FileButton,
- Image,
- Stack,
-} from "@mantine/core";
-import { IconImageInPicture, IconUpload } from "@tabler/icons-react";
-import { useState } from "react";
-
-export default function Page() {
- return (
- <>
- }
- >
-
-
- >
- );
-}
-
-function Upload() {
- const [file, setFile] = useState(null);
- const [image, setImage] = useState(null);
- const [isLoading, setLoading] = useState(false);
-
- async function onUpload() {
- if (!file) return alert("File Kosong");
- try {
- setLoading(true);
- const formData = new FormData();
- formData.append("file", file as File);
-
- const uploadPhoto = await funGlobal_UploadToStorage({
- file: file,
- dirId: "cm5ohsepe002bq4nlxeejhg7q",
- });
-
- if (uploadPhoto.success) {
- setLoading(false);
- alert("berhasil upload");
- console.log("uploadPhoto", uploadPhoto);
- } else {
- setLoading(false);
- console.log("gagal upload", uploadPhoto);
- }
- } catch (error) {
- console.error("Error upload img:", error);
- }
- }
-
- return (
- <>
-
-
- {image ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
- {
- try {
- const buffer = URL.createObjectURL(
- new Blob([new Uint8Array(await files.arrayBuffer())])
- );
-
- // if (files.size > MAX_SIZE) {
- // ComponentGlobal_NotifikasiPeringatan(
- // PemberitahuanMaksimalFile
- // );
- // return;
- // } else {
-
- // }
-
- console.log("ini buffer", buffer);
-
- setFile(files);
- setImage(buffer);
- } catch (error) {
- clientLogger.error("Upload error:", error);
- }
- }}
- accept="image/png,image/jpeg"
- >
- {(props) => (
- }
- bg={MainColor.yellow}
- color="yellow"
- c={"black"}
- >
- Upload
-
- )}
-
-
-
-
-
- >
- );
-}
diff --git a/src/app/zz-makuro/LoadDataContoh.tsx b/src/app/zz-makuro/LoadDataContoh.tsx
deleted file mode 100644
index adf8a237..00000000
--- a/src/app/zz-makuro/LoadDataContoh.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-"use client";
-import { Stack } from "@mantine/core";
-import useSwr from "swr";
-
-const fether = (url: string) =>
- fetch("https://jsonplaceholder.typicode.com" + url, {
- cache: "force-cache",
- next: {
- revalidate: 60,
- },
- }).then((res) => res.json());
-
-export default function LoadDataContoh() {
- const { data, isLoading, error, mutate, isValidating } = useSwr(
- "/posts/1",
- fether,
- {
- revalidateOnFocus: false,
- revalidateOnReconnect: false,
- refreshInterval: 1000,
- }
- );
- return (
-
- {isLoading && Loading...
}
- LoadDataContoh
- {JSON.stringify(data, null, 2)}
-
- );
-}
diff --git a/src/app/zz-makuro/get-data-example.ts b/src/app/zz-makuro/get-data-example.ts
deleted file mode 100644
index d52d9586..00000000
--- a/src/app/zz-makuro/get-data-example.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-async function getDataExample() {
- const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
- next: {
- revalidate: 60,
- },
- });
-
- return res.json();
-}
diff --git a/src/app/zz-makuro/page.tsx b/src/app/zz-makuro/page.tsx
deleted file mode 100644
index a1df80ba..00000000
--- a/src/app/zz-makuro/page.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Suspense } from "react";
-import LoadDataContoh from "./LoadDataContoh";
-
-const listMenu = [
- {
- name: "Dashboard",
- url: "/dashboard",
- icon: "dashboard",
- },
- {
- name: "Event",
- url: "/event",
- icon: "event",
- },
- {
- name: "Donasi",
- url: "/donasi",
- icon: "donasi",
- },
-];
-
-const fether = async (url: string) =>
- fetch("https://jsonplaceholder.typicode.com" + url, {
- next: {
- revalidate: 2,
- },
- }).then(async (res) => {
- const data = await res.json();
- // console.log(data);
- return data;
- });
-
-export default async function Page() {
- const data = await fether("/posts/1");
-
- return (
-
- {listMenu.map((item) => {
- return (
-
- );
- })}
- {/*
*/}
-
Loading... }>
- {JSON.stringify(data, null, 2)}
-
-
- );
-}
diff --git a/src/lib/code-otp-sender.ts b/src/lib/code-otp-sender.ts
index c7213348..cee0e5de 100644
--- a/src/lib/code-otp-sender.ts
+++ b/src/lib/code-otp-sender.ts
@@ -24,8 +24,6 @@ const sendCodeOtp = async ({
}),
});
- console.log("RES >>", res);
-
return res;
};
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
index 5cfc6aa5..13938416 100644
--- a/src/lib/prisma.ts
+++ b/src/lib/prisma.ts
@@ -45,10 +45,16 @@ async function gracefulShutdown(): Promise {
console.log("[Prisma] Semua koneksi ditutup");
}
-// Register shutdown handlers (hanya di environment Node.js)
-if (typeof process !== "undefined") {
- process.on("SIGINT", gracefulShutdown);
- process.on("SIGTERM", gracefulShutdown);
+// Register shutdown handlers (hanya di environment Node.js server)
+// Cegah duplikasi listener dengan cek listenerCount terlebih dahulu
+// IMPORTANT: Bungkus dalam check untuk mencegah error di browser
+if (typeof process !== "undefined" && typeof process.listenerCount === "function") {
+ if (process.listenerCount("SIGINT") === 0) {
+ process.on("SIGINT", gracefulShutdown);
+ }
+ if (process.listenerCount("SIGTERM") === 0) {
+ process.on("SIGTERM", gracefulShutdown);
+ }
}
export default prisma;
diff --git a/src/middleware.tsx b/src/middleware.tsx
index 0d532965..dc78b855 100644
--- a/src/middleware.tsx
+++ b/src/middleware.tsx
@@ -49,6 +49,7 @@ const CONFIG: MiddlewareConfig = {
"/auth/api/login",
"/waiting-room",
"/zCoba/*",
+ "/event/*/confirmation",
"/aset/global/main_background.png",
"/aset/logo/logo-hipmi.png",
"/aset/logo/hiconnect.png",
diff --git a/test/coba.test.ts b/test/coba.test.ts
deleted file mode 100644
index 1cc5d3e1..00000000
--- a/test/coba.test.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { prisma } from "@/lib";
-import { describe, test, expect } from "bun:test";
-
-describe("coba test", () => {
- test("coba", async () => {
- const user = await prisma.user.findMany();
- expect(user).not.toBeEmpty();
- });
-});