Compare commits

...

19 Commits

Author SHA1 Message Date
defafe694f chore(release): 1.7.3 2026-03-27 17:22:38 +08:00
cd3a9cc223 Fix server
### Issue:  process.listenerCount()
2026-03-13 16:44:25 +08:00
0eb31073b7 chore(release): 1.7.2 2026-03-13 16:43:35 +08:00
d33296d23b Fix validasi mobile
### No issue;
2026-03-11 14:12:07 +08:00
f7d05783c7 chore(release): 1.7.1 2026-03-11 13:56:09 +08:00
0f4abea990 Fix Bug & Clean Code
### No Issue"
2026-03-10 16:22:36 +08:00
a03c1fa575 Fix Docker file and Clean code
### No issue
2026-03-10 15:07:54 +08:00
fe457cd2d4 chore(release): 1.7.0 2026-03-10 15:05:46 +08:00
73cbf3640a feat: Tambahkan deep link handler untuk event confirmation
Deskripsi:
- Membuat route handler /event/[id]/confirmation untuk deep link mobile
- Menambahkan deteksi platform (iOS, Android, Web) dari user agent
- Memperbaiki Content-Type header untuk file .well-known (AASA & assetlinks)
- Menambahkan route ke public middleware agar bisa diakses tanpa auth

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

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

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

### No issue
2026-03-06 16:35:34 +08:00
4fd7bb4a17 chore(release): 1.6.9 2026-03-06 16:29:50 +08:00
b2305a35a6 Fix WA Otp
### NO Issue
2026-03-05 16:38:31 +08:00
cbfd105134 chore(release): 1.6.8 2026-03-05 14:30:03 +08:00
3e6c94d77f Usulan Commit Message
fix: Implementasi retry mechanism dan error handling untuk database connections

Deskripsi:

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

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

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

Menambahkan validasi input parameters pada beberapa endpoints

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

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

File yang diubah:

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

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

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

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

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

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

QWEN.md — Dokumentasi commit & comment standards

.env.example — Database connection pool configuration

### No Issue
2026-03-05 14:28:45 +08:00
a6c9182a01 Fix Server dengan penerapan Github build 2026-03-04 16:38:58 +08:00
453aa0a4ec chore(release): 1.6.7 2026-03-04 16:36:57 +08:00
fe37cce13e Fix publish.yml 2026-03-04 16:08:59 +08:00
ee05d0c71f Build Github 2026-03-04 15:18:01 +08:00
f8319b9ab5 Build with Github 2026-03-04 14:12:12 +08:00
70 changed files with 827 additions and 2261 deletions

View File

@@ -1,8 +1,11 @@
# ==============================
# Database
# ==============================
# Tambahkan connection_limit dan pool_timeout untuk mencegah connection exhaustion
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?connection_limit=10&pool_timeout=20"
# Connection pool settings untuk mencegah connection exhaustion:
# - connection_limit=10: Maksimal 10 koneksi per Prisma Client instance
# - pool_timeout=20: Timeout menunggu koneksi tersedia (detik)
# - connect_timeout=10: Timeout untuk membuat koneksi baru (detik)
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?connection_limit=10&pool_timeout=20&connect_timeout=10"
# ==============================
# Auth / Session

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

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

View File

@@ -2,6 +2,25 @@
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.3](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.2...v1.7.3) (2026-03-27)
## [1.7.2](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.1...v1.7.2) (2026-03-13)
## [1.7.1](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.7.0...v1.7.1) (2026-03-11)
## [1.7.0](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.9...v1.7.0) (2026-03-10)
### Features
* Tambahkan deep link handler untuk event confirmation ([73cbf36](https://wibugit.wibudev.com/wibu/hipmi/commit/73cbf3640ac795995e15448b24408b179d2a46d2))
## [1.6.9](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.8...v1.6.9) (2026-03-06)
## [1.6.8](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.7...v1.6.8) (2026-03-05)
## [1.6.7](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.6...v1.6.7) (2026-03-04)
## [1.6.6](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.5...v1.6.6) (2026-03-03)

68
Dockerfile Normal file
View File

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

64
QWEN.md
View File

@@ -120,14 +120,6 @@ The team follows a structured Git workflow:
- `style`: Styling changes
- `perf`: Performance improvements
### Code Standards
- TypeScript with strict mode enabled
- Component header comments with file description, creator, date
- Function comments with parameter and return value descriptions
- Custom type interface comments
- Error handling comments
- Complex logic comments
### Commit Message Format
```
type: Short description
@@ -140,6 +132,62 @@ Body:
References: #issue-number
```
**Example:**
```
feat: Tambahkan fitur kalkulator
Deskripsi:
- Menambahkan fungsi penambahan, pengurangan, perkalian, dan pembagian
- Memperbolehkan pengguna untuk memasukkan dua angka dan melakukan operasi matematika
Fixes #12
```
### Code Standards
- TypeScript with strict mode enabled
- Component header comments with file description, creator, date
- Function comments with parameter and return value descriptions
- Custom type interface comments
- Error handling comments
- Complex logic comments
### Comment Standards
**File Header:**
```typescript
/**
* Nama File: app.ts
* Deskripsi: Ini adalah file utama aplikasi.
* Pembuat: John Doe
* Tanggal: 27 Juli 2023
*/
```
**Function Comments:**
```typescript
/**
* Fungsi untuk menambahkan dua angka.
* @param {number} a - Angka pertama.
* @param {number} b - Angka kedua.
* @returns {number} Hasil penjumlahan a dan b.
*/
function addNumbers(a: number, b: number): number {
return a + b;
}
```
**Custom Type Comments:**
```typescript
/**
* Interface untuk merepresentasikan informasi pelanggan.
*/
interface Customer {
id: number; // ID pelanggan
name: string; // Nama pelanggan
age?: number; // Umur pelanggan (opsional)
}
```
## Project Structure
### Main Directories

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "hipmi",
"version": "1.6.6",
"version": "1.7.3",
"private": true,
"prisma": {
"seed": "bun prisma/seed.ts"

View File

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

View File

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

View File

@@ -43,9 +43,9 @@ const envSrc = path.join(__dirname, '../.env');
const envDest = path.join(standaloneDir, '.env');
if (fs.existsSync(envSrc)) {
fs.copyFileSync(envSrc, envDest);
console.log('✓ .env file copied to standalone output');
// console.log('✓ .env file copied to standalone output');
} else {
console.warn('⚠ .env file not found, skipping...');
// console.warn('⚠ .env file not found, skipping...');
console.warn(' Pastikan DATABASE_URL di-set di system environment server!');
}
@@ -54,7 +54,7 @@ const envLocalSrc = path.join(__dirname, '../.env-local');
const envLocalDest = path.join(standaloneDir, '.env-local');
if (fs.existsSync(envLocalSrc)) {
fs.copyFileSync(envLocalSrc, envLocalDest);
console.log('✓ .env-local file copied to standalone output');
// console.log('✓ .env-local file copied to standalone output');
}
console.log('✅ Build script completed!');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (
<>
<UIGlobal_LayoutTamplate
header={<UIGlobal_LayoutHeaderTamplate title="Upload" />}
>
<Upload />
</UIGlobal_LayoutTamplate>
</>
);
}
function Upload() {
const [file, setFile] = useState<File | null>(null);
const [image, setImage] = useState<any | null>(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 (
<>
<Stack>
<ComponentGlobal_BoxUploadImage>
{image ? (
<AspectRatio ratio={1 / 1} mt={5} maw={300} mx={"auto"}>
<Image style={{ maxHeight: 250 }} alt="Avatar" src={image} />
</AspectRatio>
) : (
<Center h={"100%"}>
<IconImageInPicture size={50} />
</Center>
)}
</ComponentGlobal_BoxUploadImage>
<Center>
<FileButton
onChange={async (files: any | null) => {
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) => (
<Button
{...props}
radius={"sm"}
leftIcon={<IconUpload />}
bg={MainColor.yellow}
color="yellow"
c={"black"}
>
Upload
</Button>
)}
</FileButton>
</Center>
<Button
loaderPosition="center"
loading={isLoading}
onClick={() => {
onUpload();
}}
>
Simpan
</Button>
</Stack>
</>
);
}

View File

@@ -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 (
<Stack>
{isLoading && <div>Loading...</div>}
LoadDataContoh
{JSON.stringify(data, null, 2)}
</Stack>
);
}

View File

@@ -1,9 +0,0 @@
async function getDataExample() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
next: {
revalidate: 60,
},
});
return res.json();
}

View File

@@ -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 (
<div>
{listMenu.map((item) => {
return (
<div key={item.name}>
<a href={item.url}>{item.name}</a>
</div>
);
})}
{/* <LoadDataContoh /> */}
<Suspense fallback={<div>Loading...</div>}>
{JSON.stringify(data, null, 2)}
</Suspense>
</div>
);
}

View File

@@ -669,10 +669,8 @@ const limit = pLimit(1);
export async function generate_seeder() {
try {
await Promise.all(listSeederQueue.map((fn) => limit(fn)));
await prisma.$disconnect();
} catch (error) {
console.error("error generate seeder", error);
await prisma.$disconnect();
}
return { status: 200, success: true };

View File

@@ -37,6 +37,5 @@ export async function AdminDonasi_getOneById(id: string) {
},
});
await prisma.$disconnect();
return res;
}

View File

@@ -33,12 +33,10 @@ export async function AdminDonasi_funUpdateStatusPublish(
});
if (!data) {
await prisma.$disconnect();
return { status: 400, message: "Data tidak ditemukan" };
}
revalidatePath(RouterAdminDonasi.table_review);
await prisma.$disconnect();
return {
data: data,

View File

@@ -36,12 +36,10 @@ export default async function adminNotifikasi_funCreateToAllUser({
},
});
if (!create) {
await prisma.$disconnect();
return { status: 400, message: "Gagal mengirim notifikasi" };
}
}
await prisma.$disconnect();
return {
status: 201,
message: "Berhasil mengirim notifikasi",

View File

@@ -25,10 +25,8 @@ export default async function adminNotifikasi_funCreateToUser({
});
if (!create) {
await prisma.$disconnect();
return { status: 400, message: "Gagal mengirim notifikasi" };
}
await prisma.$disconnect();
return { status: 201, message: "Berhasil mengirim notifikasi" };
}

View File

@@ -9,9 +9,6 @@ export async function donasi_checkStatus({ id }: { id: string }) {
},
});
await prisma.$disconnect();
if (checkStatus?.donasiMaster_StatusDonasiId == "2") return true;
return false;

View File

@@ -7,23 +7,22 @@ const sendCodeOtp = async ({
codeOtp?: string;
newMessage?: string;
}) => {
const msg = newMessage || `HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya.\n\n>> Kode OTP anda: ${codeOtp}.`;
const enCode = encodeURIComponent(msg);
const res = await fetch(
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${nomor}&text=${enCode}`,
{
cache: "no-cache",
headers: {
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
},
const msg =
newMessage ||
`HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya.\n\n>> Kode OTP anda: ${codeOtp}.`;
const enCode = msg;
const res = await fetch(`https://otp.wibudev.com/api/wa/send-text`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
},
);
// const res = await fetch(
// `https://wa.wibudev.com/code?nom=${nomor}&text=HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya.
// \n
// >> Kode OTP anda: ${codeOtp}.
// `,
// );
body: JSON.stringify({
number: nomor,
text: enCode,
}),
});
return res;
};

View File

@@ -1,21 +1,60 @@
import { PrismaClient } from "@prisma/client";
/**
* Instance global Prisma client untuk connection pooling
* Menggunakan pattern globalThis untuk mencegah multiple instance selama:
* - Hot module replacement (HMR) di development
* - Multiple import di seluruh aplikasi
* - Server-side rendering di Next.js
*/
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is required but not found in environment variables");
}
// Konfigurasi connection pool via parameter query DATABASE_URL:
// connection_limit=10&pool_timeout=20&connect_timeout=10
const prisma =
global.prisma ??
globalThis.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["error", "warn", "query"] : ["error", "warn"],
log:
process.env.NODE_ENV === "development"
? ["error", "warn"]
: ["error"],
datasources: {
db: {
url: process.env.DATABASE_URL,
},
},
});
// Selalu assign ke global agar hanya ada 1 instance (dev: cegah hot-reload, prod: cegah multiple instances)
global.prisma = prisma;
// Hanya assign ke global di development untuk mencegah multiple instance saat HMR
// Di production, ini di-skip karena tidak ada HMR
if (process.env.NODE_ENV !== "production") {
globalThis.prisma = prisma;
}
/**
* Handler graceful shutdown untuk koneksi Prisma
* Panggil ini HANYA saat terminasi process (SIGINT/SIGTERM)
* JANGAN panggil $disconnect() setelah query individual
*/
async function gracefulShutdown(): Promise<void> {
console.log("[Prisma] Menutup koneksi database...");
await prisma.$disconnect();
console.log("[Prisma] Semua koneksi ditutup");
}
// Register shutdown handlers (hanya di environment Node.js)
// Cegah duplikasi listener dengan cek listenerCount terlebih dahulu
if (typeof process !== "undefined") {
if (process.listenerCount("SIGINT") === 0) {
process.on("SIGINT", gracefulShutdown);
}
if (process.listenerCount("SIGTERM") === 0) {
process.on("SIGTERM", gracefulShutdown);
}
}
export default prisma;
export { prisma };

View File

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

204
zCoba.js
View File

@@ -1,204 +0,0 @@
const { PrismaClient } = require('@prisma/client')
const axios = require('axios')
const prisma = new PrismaClient()
// Daftar contoh data
const donationDataList = [
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Bantuan Pendidikan Anak-anak Kurang Mampu",
"target": 50000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank Central Asia",
"rekening": "1234567890",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Kami ingin membantu anak-anak kurang mampu mendapatkan pendidikan yang layak.",
"cerita": "Pendidikan adalah hak dasar setiap anak. Namun, banyak anak-anak di pelosok negeri yang tidak bisa menikmati pendidikan karena keterbatasan ekonomi. Melalui kampanye ini, kami ingin mengumpulkan dana untuk membantu biaya pendidikan mereka.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj0"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Pembangunan Masjid di Desa Terpencil",
"target": 100000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank Mandiri",
"rekening": "0987654321",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Membangun masjid untuk masyarakat di daerah terpencil yang belum memiliki tempat ibadah.",
"cerita": "Di sebuah desa terpencil, masyarakat setiap hari harus berjalan jauh untuk bisa melaksanakan sholat berjamaah. Kami ingin membantu membangun masjid di tengah-tengah mereka agar ibadah bisa dilakukan dengan lebih tenang dan khusyuk.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj1"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Bantuan Korban Bencana Alam",
"target": 75000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank Rakyat Indonesia",
"rekening": "5678901234",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Membantu meringankan beban korban bencana alam berupa kebutuhan pokok dan kebutuhan darurat.",
"cerita": "Beberapa wilayah dilanda bencana banjir dan tanah longsor. Masyarakat kehilangan harta benda dan membutuhkan bantuan segera. Dana yang terkumpul akan digunakan untuk menyediakan makanan, obat-obatan, dan kebutuhan pokok lainnya.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj2"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Pengadaan Alat Medis Rumah Sakit",
"target": 150000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank Negara Indonesia",
"rekening": "4321098765",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Meningkatkan kualitas pelayanan kesehatan dengan menyediakan alat medis yang lebih baik.",
"cerita": "Rumah sakit daerah kekurangan alat medis untuk melayani pasien. Melalui donasi ini, kami ingin membantu pengadaan alat-alat medis penting seperti ventilator, USG, dan alat laboratorium untuk meningkatkan kualitas pelayanan kesehatan.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj3"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Program Beasiswa Mahasiswa Berprestasi",
"target": 80000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank Danamon",
"rekening": "1122334455",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Memberikan kesempatan kepada mahasiswa berprestasi untuk melanjutkan pendidikan tanpa beban biaya.",
"cerita": "Banyak mahasiswa berprestasi yang tidak mampu melanjutkan pendidikan karena keterbatasan biaya. Program beasiswa ini akan membantu mereka menyelesaikan kuliah hingga sarjana.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj4"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Pengadaan Air Bersih untuk Desa Kekeringan",
"target": 60000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank Permata",
"rekening": "6677889900",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Menyediakan akses air bersih bagi masyarakat yang tinggal di daerah rawan kekeringan.",
"cerita": "Beberapa desa mengalami kekeringan setiap tahunnya, membuat warga kesulitan mendapatkan air bersih. Kami ingin membangun sumur bor dan sistem distribusi air untuk membantu masyarakat setempat.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj5"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Pengobatan Gratis untuk Warga Tidak Mampu",
"target": 40000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank Panin",
"rekening": "9988776655",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Memberikan layanan kesehatan gratis bagi warga yang tidak mampu membayar biaya pengobatan.",
"cerita": "Banyak warga yang menunda pengobatan karena keterbatasan biaya. Melalui program ini, kami akan menyelenggarakan pengobatan gratis secara berkala di berbagai wilayah.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj6"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Pembangunan Taman Bacaan Masyarakat",
"target": 35000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank Mega",
"rekening": "1357924680",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Membangun taman bacaan untuk meningkatkan minat baca masyarakat di wilayah pedesaan.",
"cerita": "Minat baca masyarakat di pedesaan masih rendah karena keterbatasan akses buku. Taman bacaan ini akan menyediakan ribuan buku gratis dan ruang baca yang nyaman untuk semua usia.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj7"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Pelatihan Keterampilan untuk Pengangguran",
"target": 55000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank CIMB Niaga",
"rekening": "2468135790",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Memberikan pelatihan keterampilan untuk membantu pengangguran mendapatkan pekerjaan atau usaha mandiri.",
"cerita": "Angka pengangguran masih tinggi di beberapa wilayah. Program pelatihan ini akan memberikan keterampilan yang dibutuhkan pasar kerja, seperti menjahit, memasak, teknologi informasi, dan lainnya.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj8"
}
},
{
"data": {
"authorId": "cmha6wb9w0001cfndwl9fcse6",
"title": "Renovasi Gedung Sekolah Rusak",
"target": 90000000,
"donasiMaster_DurasiId": 3,
"donasiMaster_KategoriId": 3,
"namaBank": "Bank OCBC NISP",
"rekening": "1029384756",
"imageId": "cm60j9q3m000xc9dc584v8rh8",
"pembukaan": "Merestrukturasi gedung sekolah yang rusak agar siswa bisa belajar dengan aman dan nyaman.",
"cerita": "Banyak gedung sekolah yang rusak parah dan membahayakan keselamatan siswa. Dana dari kampanye ini akan digunakan untuk renovasi dan perbaikan gedung sekolah yang membutuhkan.",
"imageCeritaId": "cm60j9q3m000xc9dc584v8rj9"
}
}
];
async function sendDonationData() {
const baseUrl = 'http://localhost:3000/api/mobile/donation'; // Sesuaikan dengan URL server Anda
const headers = {
'Content-Type': 'application/json',
};
for (let i = 0; i < donationDataList.length; i++) {
try {
console.log(`Mengirim data ke-${i + 1}...`);
const response = await axios.post(`${baseUrl}?category=permanent`, donationDataList[i], {
headers: headers
});
console.log(`Data ke-${i + 1} berhasil dikirim:`, response.data);
} catch (error) {
console.error(`Error saat mengirim data ke-${i + 1}:`, error.response?.data || error.message);
}
}
}
async function main() {
// Menjalankan fungsi untuk mengirim data donasi
await sendDonationData();
// Fungsi asli untuk update notifikasi
const result = await prisma.notifikasi.updateMany({
where: {
recipientId: 'cmha7p6yc0000cfoe5w2e7gdr',
},
data: {
isRead: false,
readAt: null,
},
})
console.log(`✅ Rows affected: ${result.count}`)
}
main()
.catch((err) => {
console.error('❌ Error:', err)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})