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 ( - <> - - logo - - {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 ( - <> - - logo - - {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 - - - - 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 = `sticker`; - - // 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) => ( - - - {item.name} 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 ? ( - - Avatar - - ) : ( -
- -
- )} -
- -
- { - 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) => ( - - )} - -
- - -
- - ); -} 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(); - }); -});