Compare commits
248 Commits
nico/12-de
...
stg
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a74a1f683 | |||
| b673e36a45 | |||
| 62aa9b63b2 | |||
| 58ab306428 | |||
| 97902f6277 | |||
| ef7d1752de | |||
| f9de4b7a35 | |||
| 13873c9fe7 | |||
| 03b084d9d4 | |||
| 5df9698599 | |||
| 3d3e5ffc87 | |||
| e80e333eed | |||
| b1289831f3 | |||
| 3c4e273e26 | |||
| de4563c914 | |||
| 11ff5f5c01 | |||
| ed44222594 | |||
| fd7579d6d3 | |||
| e7c3c020c2 | |||
| 6873e84848 | |||
| 74dc9e5c18 | |||
| 04001c905b | |||
| 656ffcc561 | |||
| 76ffa662c5 | |||
| 46423409fd | |||
| 2edf5e9b11 | |||
| af368eeee0 | |||
| e104cd8fcc | |||
| 50801e5c8a | |||
| 62a9a49502 | |||
| 80186bf493 | |||
| e669dcee25 | |||
| d84edc44f5 | |||
| 8b14c6ce44 | |||
| 5e822f0b05 | |||
| 34a37dc63b | |||
| 0e6f7e1769 | |||
| feb853d06e | |||
| 3de412afe0 | |||
| 87d234e57f | |||
| fd18a22834 | |||
| 3e8b961e52 | |||
| 82d779e5e0 | |||
| a6517166cb | |||
| 483b6be677 | |||
| f8dad0dbcd | |||
| 74301fe074 | |||
| 8b19abc628 | |||
| e73798a0f2 | |||
| 1a91f3c9ad | |||
| 9b74592101 | |||
| 55f4b94082 | |||
| b403bc754c | |||
| 67b87f145e | |||
| dd09d7c90a | |||
| 59ae8ad039 | |||
| c012d5778c | |||
| af31bd8aef | |||
| 721357adcf | |||
| 39776ec355 | |||
| 50a7356618 | |||
| 4494dd98ef | |||
| 970949a68b | |||
| 8777c45a44 | |||
| 42bcba6c96 | |||
| d1d54e5c25 | |||
| 0a4b85fd82 | |||
| b751f031cd | |||
| a3940321a7 | |||
| 3cd6fcbd81 | |||
| 7d9b7b0c60 | |||
| 0806eb2308 | |||
|
|
6064ef0759 | ||
| 1c00c326c9 | |||
| 51ce823b45 | |||
| 4eba96140d | |||
| f6f0e10935 | |||
| 2108f403aa | |||
| 4dfcf20322 | |||
| c6c3eebadf | |||
|
|
6d26ace8ab | ||
|
|
0dabc204bc | ||
|
|
e8f8b51686 | ||
|
|
a4db3a149d | ||
|
|
fece983ac5 | ||
| 8b7eef5fee | |||
| 8b22d01e0d | |||
| dc13e37a02 | |||
| 2d2cbef29b | |||
| 8c8a96b830 | |||
| dc3eccacbf | |||
| ffe94992e5 | |||
|
|
f5566bca2c | ||
|
|
ba964df32c | ||
|
|
df3f382a97 | ||
| 4fb522f88f | |||
| 85332a8225 | |||
| 3fe2a5ccab | |||
| 363bfa65fb | |||
| dccf590cbf | |||
| f076b81d14 | |||
| b5ea3216e0 | |||
| 64b116588b | |||
| 63161e1a39 | |||
| 8b8c65dd1e | |||
| 159fb3cec6 | |||
| 4821934224 | |||
| ee39b88b00 | |||
| ce46d3b5f7 | |||
| 144ac37e12 | |||
| f90477ed63 | |||
| 4a7811e06f | |||
| f63aaf916d | |||
| 3803c79c95 | |||
| 2d901912ea | |||
| a791efe76c | |||
| e9f7bc2043 | |||
| 6712da9ac2 | |||
| ac11a9367c | |||
| 67e5ceb254 | |||
| 65942ac9d2 | |||
| e0436cc384 | |||
| 63682e47b6 | |||
| f4705690a9 | |||
| 239771a714 | |||
| 03451195c8 | |||
| 597af7e716 | |||
| 0a8a026b94 | |||
| a5bd91b580 | |||
|
|
7368a367f4 | ||
|
|
ed664d5b10 | ||
| ae3187804e | |||
|
|
0ba30aa5b2 | ||
|
|
790d6535e5 | ||
|
|
46ce16ae97 | ||
| 91e32f3f1c | |||
| 4d03908f23 | |||
| 0563f9664f | |||
| 961cc32057 | |||
| fe7672e09f | |||
| 341ff5779f | |||
| 69f7b4c162 | |||
| 409ad4f1a2 | |||
| 55ea3c473a | |||
| 0160fa636d | |||
| a152eaf984 | |||
| 3684e83187 | |||
| 223b85a714 | |||
| 77c54b5c8a | |||
| f1729151b3 | |||
| 8e8c133eea | |||
| 1e7acac193 | |||
| bb80b0ecc1 | |||
| 42dcbcfb22 | |||
| 22de1aa1f3 | |||
| b1d28a8322 | |||
| b86a3a85c3 | |||
| fd63bb0fd4 | |||
| f2c9a922a6 | |||
| 92b24440fe | |||
| f0558aa0d0 | |||
| 8132609ccb | |||
| 1ddc1d7eac | |||
| aa354992e7 | |||
| d43b07c2ef | |||
| 9678e6979b | |||
| b35874b120 | |||
| 1b59d6bf09 | |||
| b69df2454e | |||
| eb1ad54db6 | |||
| df198c320a | |||
| 21ec3ad1c1 | |||
| f550e29a75 | |||
| 3a115908c4 | |||
| bb7384f1e5 | |||
| 5ff791642c | |||
| df154806f7 | |||
| b803c7a90c | |||
| 25000d0b0f | |||
| fb2fe67c23 | |||
| bbd52fb6f5 | |||
| 51460558d4 | |||
| 358ff14efe | |||
| d105ceeb6b | |||
| 6c36a15290 | |||
| da585dde99 | |||
| c865aee766 | |||
| 8afbaabd91 | |||
| 273dfdfd09 | |||
| f0425cfc47 | |||
| 1d1d8e50dc | |||
| c2ad515366 | |||
| 092afe67d2 | |||
| d9ce4aac6d | |||
| 2d9170705d | |||
| 3fcfec22fb | |||
| fdf9a951a4 | |||
| 6ca1e032a6 | |||
| ca74029688 | |||
| 78c55a8a71 | |||
| 1a8fc1a670 | |||
| 17b20e0d40 | |||
| 184854d273 | |||
| 903dc74cca | |||
| 503da91ce6 | |||
| 19235f0791 | |||
| daaed8089b | |||
| 61de7d8d33 | |||
| f436aa2ef0 | |||
| 8fb85ce56c | |||
| 50bc54ceca | |||
| 1f98b6993d | |||
| f0f201c853 | |||
| 29065cb3e2 | |||
| f3a10d63d1 | |||
| bf20cd55e8 | |||
| af60bcd6fc | |||
| 7a42bec63b | |||
| dc8793e3ae | |||
| 44c421129e | |||
| c8484357cb | |||
| 342e9bbc65 | |||
| ddff427926 | |||
| 00c8caade4 | |||
| 0209f49449 | |||
| 344c6ada6d | |||
| 11acd04419 | |||
| 8d49213b68 | |||
| 96911e3cf1 | |||
| 9950c28b9b | |||
| fa0f3538d1 | |||
| 2778f53aff | |||
| 37ac91d4f4 | |||
| 217f4a9a3b | |||
| 5d6a7437ed | |||
| 752a6cabee | |||
| 134ddc6154 | |||
| 28979c6b49 | |||
| b2066caa13 | |||
| 023c77d636 | |||
| 9bf3ec72cf | |||
| f359f5b1ce | |||
| 1c1e8fb190 | |||
| 54f83da3b8 | |||
| f8985c550f | |||
| e3d909e760 | |||
| 16a8df50c1 | |||
| d66a952d4c |
47
.dockerignore
Normal file
47
.dockerignore
Normal file
@@ -0,0 +1,47 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
bun-debug.log*
|
||||
|
||||
# Docker files
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Markdown/Documentation
|
||||
README.md
|
||||
GEMINI.md
|
||||
AGENTS.md
|
||||
AUDIT_REPORT.md
|
||||
QWEN.md
|
||||
NOTE.md
|
||||
task-project-apbdes.md
|
||||
MUSIK_CREATE_ANALYSIS.md
|
||||
darkMode.md
|
||||
/test-results
|
||||
/playwright-report
|
||||
/tmp_assets
|
||||
/foldergambar
|
||||
/googleapi
|
||||
/xx
|
||||
/xx.ts
|
||||
/xx.txt
|
||||
/test.txt
|
||||
/x.json
|
||||
/x.sh
|
||||
/xcoba.ts
|
||||
/xcoba2.ts
|
||||
/gambar.ttx
|
||||
/test-berita-state.ts
|
||||
44
.env.example
Normal file
44
.env.example
Normal file
@@ -0,0 +1,44 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/desa-darmasaba?schema=public"
|
||||
|
||||
# Seafile Configuration (File Storage)
|
||||
SEAFILE_TOKEN=your_seafile_token
|
||||
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||
SEAFILE_URL=https://your-seafile-instance.com
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||
|
||||
# Upload Configuration
|
||||
WIBU_UPLOAD_DIR=uploads
|
||||
WIBU_DOWNLOAD_DIR=./download
|
||||
|
||||
# WhatsApp Server Configuration
|
||||
WA_SERVER_TOKEN=your_whatsapp_server_token
|
||||
|
||||
# Application Configuration
|
||||
# IMPORTANT: For staging/production, set this to your actual domain
|
||||
# Local development: NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
# Staging: NEXT_PUBLIC_BASE_URL=https://desa-darmasaba-stg.wibudev.com
|
||||
# Production: NEXT_PUBLIC_BASE_URL=https://your-production-domain.com
|
||||
# Or use relative URL '/' for automatic protocol/domain detection (recommended)
|
||||
NEXT_PUBLIC_BASE_URL=/
|
||||
|
||||
# Email Configuration (for notifications/subscriptions)
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_email_app_password
|
||||
|
||||
# Session Configuration
|
||||
BASE_SESSION_KEY=your_session_key_generate_secure_random_string
|
||||
BASE_TOKEN_KEY=your_jwt_secret_key_generate_secure_random_string
|
||||
|
||||
# Telegram Bot Configuration (for notifications)
|
||||
BOT_TOKEN=your_telegram_bot_token
|
||||
CHAT_ID=your_telegram_chat_id
|
||||
|
||||
# Session Password (for iron-session)
|
||||
SESSION_PASSWORD="your_session_password_min_32_characters_long_secure"
|
||||
|
||||
# ElevenLabs API Key (for TTS features - optional)
|
||||
ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
||||
|
||||
# Environment (optional, defaults to development)
|
||||
# NODE_ENV=development
|
||||
81
.gemini/hooks/telegram-notify.ts
Executable file
81
.gemini/hooks/telegram-notify.ts
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bun
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Function to manually load .env from project root if process.env is missing keys
|
||||
function loadEnv() {
|
||||
const envPath = join(process.cwd(), ".env");
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, "utf-8");
|
||||
const lines = envContent.split("\n");
|
||||
for (const line of lines) {
|
||||
if (line && !line.startsWith("#")) {
|
||||
const [key, ...valueParts] = line.split("=");
|
||||
if (key && valueParts.length > 0) {
|
||||
const value = valueParts.join("=").trim().replace(/^["']|["']$/g, "");
|
||||
process.env[key.trim()] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// Ensure environment variables are loaded
|
||||
loadEnv();
|
||||
|
||||
const inputRaw = readFileSync(0, "utf-8");
|
||||
if (!inputRaw) return;
|
||||
|
||||
let finalText = "";
|
||||
let sessionId = "web-desa-darmasaba";
|
||||
|
||||
try {
|
||||
// Try parsing as JSON first
|
||||
const input = JSON.parse(inputRaw);
|
||||
sessionId = input.session_id || "web-desa-darmasaba";
|
||||
finalText = typeof input === "string" ? input : (input.response || input.text || JSON.stringify(input));
|
||||
} catch {
|
||||
// If not JSON, use raw text
|
||||
finalText = inputRaw;
|
||||
}
|
||||
|
||||
const BOT_TOKEN = process.env.BOT_TOKEN;
|
||||
const CHAT_ID = process.env.CHAT_ID;
|
||||
|
||||
if (!BOT_TOKEN || !CHAT_ID) {
|
||||
console.error("Missing BOT_TOKEN or CHAT_ID in environment variables");
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
`✅ *Gemini Task Selesai*\n\n` +
|
||||
`🆔 Session: \`${sessionId}\` \n\n` +
|
||||
`🧠 Output:\n${finalText.substring(0, 3500)}`;
|
||||
|
||||
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: CHAT_ID,
|
||||
text: message,
|
||||
parse_mode: "Markdown",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
console.error("Telegram API Error:", errorData);
|
||||
} else {
|
||||
console.log("Notification sent successfully!");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ status: "continue" }));
|
||||
} catch (err) {
|
||||
console.error("Hook Error:", err);
|
||||
process.stdout.write(JSON.stringify({ status: "continue" }));
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
17
.gemini/settings.json
Normal file
17
.gemini/settings.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"hooks": {
|
||||
"AfterAgent": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "telegram-notify",
|
||||
"type": "command",
|
||||
"command": "bun $GEMINI_PROJECT_DIR/.gemini/hooks/telegram-notify.ts",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
219
.github/workflows/build.yml
vendored
219
.github/workflows/build.yml
vendored
@@ -1,219 +0,0 @@
|
||||
name: Build And Save Log
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: "Target environment (e.g., staging, production)"
|
||||
required: true
|
||||
default: "staging"
|
||||
version:
|
||||
description: "Version to deploy"
|
||||
required: false
|
||||
default: "latest"
|
||||
|
||||
env:
|
||||
APP_NAME: desa-darmasaba-action
|
||||
WA_PHONE: "6289697338821,6289697338822"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd="pg_isready"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
|
||||
steps:
|
||||
# Checkout kode sumber
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Setup Bun
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
# Cache dependencies
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .bun
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
# Step 1: Set BRANCH_NAME based on event type
|
||||
- name: Set BRANCH_NAME
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Step 2: Generate APP_VERSION dynamically
|
||||
- name: Set APP_VERSION
|
||||
run: echo "APP_VERSION=${{ github.sha }}---$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
|
||||
|
||||
# Step 3: Kirim notifikasi ke API build Start
|
||||
- name: Notify start build
|
||||
run: |
|
||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
||||
for PHONE in "${PHONES[@]}"; do
|
||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:start\nApp:${{ env.APP_NAME }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}'))")
|
||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
||||
done
|
||||
|
||||
# Install dependencies
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
# Konfigurasi environment variable untuk PostgreSQL dan variabel tambahan
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
|
||||
echo "PORT=3000" >> .env
|
||||
echo "NEXT_PUBLIC_WIBU_URL=localhost:3000" >> .env
|
||||
echo "WIBU_UPLOAD_DIR=/uploads" >> .env
|
||||
|
||||
# Create log file
|
||||
- name: Create log file
|
||||
run: touch build.txt
|
||||
|
||||
# Migrasi database menggunakan Prisma
|
||||
- name: Apply Prisma schema to database
|
||||
run: bun prisma db push >> build.txt 2>&1
|
||||
|
||||
# Seed database (opsional)
|
||||
- name: Seed database
|
||||
run: |
|
||||
bun prisma db seed >> build.txt 2>&1 || echo "Seed failed or no seed data found. Continuing without seed." >> build.txt
|
||||
|
||||
# Build project
|
||||
- name: Build project
|
||||
run: bun run build >> build.txt 2>&1
|
||||
|
||||
# Ensure project directory exists
|
||||
- name: Ensure /var/www/projects/${{ env.APP_NAME }} exists
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
script: |
|
||||
mkdir -p /var/www/projects/${{ env.APP_NAME }}
|
||||
|
||||
# Deploy to a new version directory
|
||||
- name: Deploy to VPS (New Version)
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
source: "."
|
||||
target: "/var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}"
|
||||
|
||||
# Set up environment variables
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
rm -r .env
|
||||
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5433/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
|
||||
echo "NEXT_PUBLIC_WIBU_URL=${{ env.APP_NAME }}" >> .env
|
||||
echo "WIBU_UPLOAD_DIR=/var/www/projects/${{ env.APP_NAME }}/uploads" >> .env
|
||||
|
||||
# Kirim file .env ke server
|
||||
- name: Upload .env to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
source: ".env"
|
||||
target: "/var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}/"
|
||||
|
||||
# manage deployment
|
||||
- name: manage deployment
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
script: |
|
||||
|
||||
# Source ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# Find an available port
|
||||
PORT=$(curl -s -X GET https://wibu-bot.wibudev.com/api/find-port | jq -r '.[0]')
|
||||
if [ -z "$PORT" ] || ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
|
||||
echo "Invalid or missing port from API."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# manage deployment
|
||||
cd /var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}
|
||||
|
||||
# Create uploads directory
|
||||
mkdir -p /var/www/projects/${{ env.APP_NAME }}/uploads
|
||||
|
||||
# Install dependencies
|
||||
bun install --production
|
||||
|
||||
# Apply database schema
|
||||
if ! bun prisma db push; then
|
||||
echo "Database migration failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Seed database (optional)
|
||||
bun prisma db seed || echo "tidak membutuhkan seed"
|
||||
|
||||
# Restart the application
|
||||
pm2 reload ${{ env.APP_NAME }} || pm2 start "bun run start --port $PORT" --name "${{ env.APP_NAME }}-$PORT" --namespace "${{ env.APP_NAME }}"
|
||||
|
||||
# Step 4: Set BUILD_STATUS based on success or failure
|
||||
- name: Set BUILD_STATUS
|
||||
if: success()
|
||||
run: echo "BUILD_STATUS=success" >> $GITHUB_ENV
|
||||
|
||||
- name: Set BUILD_STATUS on failure
|
||||
if: failure()
|
||||
run: echo "BUILD_STATUS=failed" >> $GITHUB_ENV
|
||||
|
||||
# Update status log
|
||||
- name: Update status log
|
||||
if: always()
|
||||
run: |
|
||||
echo "=====================" >> build.txt
|
||||
echo "BUILD_STATUS=${{ env.BUILD_STATUS }}" >> build.txt
|
||||
echo "APP_NAME=${{ env.APP_NAME }}" >> build.txt
|
||||
echo "APP_VERSION=${{ env.APP_VERSION }}" >> build.txt
|
||||
echo "=====================" >> build.txt
|
||||
|
||||
# Upload log to 0x0.st
|
||||
- name: Upload log to 0x0.st
|
||||
id: upload_log
|
||||
if: always()
|
||||
run: |
|
||||
LOG_URL=$(curl -F "file=@build.txt" https://wibu-bot.wibudev.com/api/file )
|
||||
echo "LOG_URL=$LOG_URL" >> $GITHUB_ENV
|
||||
|
||||
# Kirim notifikasi ke API
|
||||
- name: Notify build success via API
|
||||
if: always()
|
||||
run: |
|
||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
||||
for PHONE in "${PHONES[@]}"; do
|
||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:${{ env.BUILD_STATUS }}\nApp:${{ env.APP_NAME }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}\nLog:${{ env.LOG_URL }}'))")
|
||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
||||
done
|
||||
56
.github/workflows/docker-publish.yml
vendored
Normal file
56
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Publish Docker to GHCR
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
jobs:
|
||||
publish:
|
||||
name: Build & Push to GHCR
|
||||
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: Extract tag name
|
||||
id: meta
|
||||
run: echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- 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: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
106
.github/workflows/publish.yml
vendored
Normal file
106
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: Publish Docker to GHCR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stack_env:
|
||||
description: "stack env"
|
||||
required: true
|
||||
type: choice
|
||||
default: "dev"
|
||||
options:
|
||||
- dev
|
||||
- prod
|
||||
- stg
|
||||
tag:
|
||||
description: "Image tag (e.g. 1.0.0)"
|
||||
required: true
|
||||
default: "1.0.0"
|
||||
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
|
||||
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 branch ${{ github.event.inputs.stack_env }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.stack_env }}
|
||||
|
||||
- name: Checkout scripts from main
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
path: .ci
|
||||
sparse-checkout: .github/workflows/script
|
||||
|
||||
- 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.stack_env }}-${{ github.event.inputs.tag }}
|
||||
type=raw,value=${{ github.event.inputs.stack_env }}-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
|
||||
|
||||
- name: Notify success
|
||||
if: success()
|
||||
run: bash ./.ci/.github/workflows/script/notify.sh
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NOTIFY_STATUS: success
|
||||
NOTIFY_WORKFLOW: "Publish Docker"
|
||||
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
|
||||
|
||||
- name: Notify failure
|
||||
if: failure()
|
||||
run: bash ./.ci/.github/workflows/script/notify.sh
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NOTIFY_STATUS: failure
|
||||
NOTIFY_WORKFLOW: "Publish Docker"
|
||||
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
|
||||
60
.github/workflows/re-pull.yml
vendored
Normal file
60
.github/workflows/re-pull.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Re-Pull Docker
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stack_name:
|
||||
description: "stack name"
|
||||
required: true
|
||||
type: string
|
||||
stack_env:
|
||||
description: "stack env"
|
||||
required: true
|
||||
type: choice
|
||||
default: "dev"
|
||||
options:
|
||||
- dev
|
||||
- stg
|
||||
- prod
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Re-Pull Docker ${{ github.event.inputs.stack_name }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout scripts from main
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
sparse-checkout: .github/workflows/script
|
||||
|
||||
- name: Deploy ke Portainer
|
||||
run: bash ./.github/workflows/script/re-pull.sh
|
||||
env:
|
||||
PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }}
|
||||
PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }}
|
||||
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
|
||||
STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}
|
||||
|
||||
- name: Notify success
|
||||
if: success()
|
||||
run: bash ./.github/workflows/script/notify.sh
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NOTIFY_STATUS: success
|
||||
NOTIFY_WORKFLOW: "Re-Pull Docker"
|
||||
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
|
||||
|
||||
- name: Notify failure
|
||||
if: failure()
|
||||
run: bash ./.github/workflows/script/notify.sh
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NOTIFY_STATUS: failure
|
||||
NOTIFY_WORKFLOW: "Re-Pull Docker"
|
||||
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
|
||||
26
.github/workflows/script/notify.sh
vendored
Normal file
26
.github/workflows/script/notify.sh
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}"
|
||||
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}"
|
||||
: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}"
|
||||
: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}"
|
||||
|
||||
if [ "$NOTIFY_STATUS" = "success" ]; then
|
||||
ICON="✅"
|
||||
TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!"
|
||||
else
|
||||
ICON="❌"
|
||||
TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!"
|
||||
fi
|
||||
|
||||
if [ -n "$NOTIFY_DETAIL" ]; then
|
||||
TEXT="${TEXT}
|
||||
${NOTIFY_DETAIL}"
|
||||
fi
|
||||
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n \
|
||||
--arg chat_id "$TELEGRAM_CHAT_ID" \
|
||||
--arg text "$TEXT" \
|
||||
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')"
|
||||
120
.github/workflows/script/re-pull.sh
vendored
Normal file
120
.github/workflows/script/re-pull.sh
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
|
||||
: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}"
|
||||
: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}"
|
||||
: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}"
|
||||
: "${STACK_NAME:?STACK_NAME tidak di-set}"
|
||||
|
||||
# Timeout total: MAX_RETRY * SLEEP_INTERVAL detik
|
||||
MAX_RETRY=60 # 60 × 10s = 10 menit
|
||||
SLEEP_INTERVAL=10
|
||||
|
||||
echo "🔐 Autentikasi ke Portainer..."
|
||||
TOKEN=$(curl -s -X POST "https://${PORTAINER_URL}/api/auth" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \
|
||||
| jq -r .jwt)
|
||||
|
||||
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||
echo "❌ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔍 Mencari stack: $STACK_NAME..."
|
||||
STACK=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
| jq ".[] | select(.Name == \"$STACK_NAME\")")
|
||||
|
||||
if [ -z "$STACK" ]; then
|
||||
echo "❌ Stack '$STACK_NAME' tidak ditemukan di Portainer!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STACK_ID=$(echo "$STACK" | jq -r .Id)
|
||||
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
|
||||
ENV=$(echo "$STACK" | jq '.Env // []')
|
||||
|
||||
# ── Catat container ID lama sebelum redeploy ──────────────────────────────────
|
||||
echo "📸 Mencatat container aktif sebelum redeploy..."
|
||||
CONTAINERS_BEFORE=$(curl -s -X GET \
|
||||
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
|
||||
-H "Authorization: Bearer ${TOKEN}")
|
||||
|
||||
OLD_IDS=$(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Id] | join(",")')
|
||||
echo " Container lama: $(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Names[0]] | join(", ")')"
|
||||
|
||||
# ── Ambil compose file lalu trigger redeploy ─────────────────────────────────
|
||||
echo "📄 Mengambil compose file..."
|
||||
STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
| jq -r .StackFileContent)
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg content "$STACK_FILE" \
|
||||
--argjson env "$ENV" \
|
||||
'{stackFileContent: $content, env: $env, pullImage: true}')
|
||||
|
||||
echo "🚀 Triggering redeploy $STACK_NAME (pull latest image)..."
|
||||
HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \
|
||||
-X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
if [ "$HTTP_STATUS" != "200" ]; then
|
||||
echo "❌ Redeploy gagal! HTTP Status: $HTTP_STATUS"
|
||||
cat /tmp/portainer_response.json | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "⏳ Menunggu image selesai di-pull dan container baru running..."
|
||||
echo " (Timeout: $((MAX_RETRY * SLEEP_INTERVAL)) detik)"
|
||||
|
||||
COUNT=0
|
||||
while [ $COUNT -lt $MAX_RETRY ]; do
|
||||
sleep $SLEEP_INTERVAL
|
||||
COUNT=$((COUNT + 1))
|
||||
|
||||
CONTAINERS=$(curl -s -X GET \
|
||||
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
|
||||
-H "Authorization: Bearer ${TOKEN}")
|
||||
|
||||
# Container baru = ID tidak ada di daftar container lama
|
||||
NEW_RUNNING=$(echo "$CONTAINERS" | jq \
|
||||
--arg old "$OLD_IDS" \
|
||||
'[.[] | select(.State == "running" and ((.Id) as $id | ($old | split(",") | index($id)) == null))] | length')
|
||||
|
||||
FAILED=$(echo "$CONTAINERS" | jq \
|
||||
'[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not))] | length')
|
||||
|
||||
echo "🔄 [$((COUNT * SLEEP_INTERVAL))s / $((MAX_RETRY * SLEEP_INTERVAL))s] Container baru running: ${NEW_RUNNING} | Gagal: ${FAILED}"
|
||||
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status) | id: \(.Id[:12])"'
|
||||
|
||||
if [ "$FAILED" -gt "0" ]; then
|
||||
echo ""
|
||||
echo "❌ Ada container yang crash!"
|
||||
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not)) | " → \(.Names[0]) | \(.Status)"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$NEW_RUNNING" -gt "0" ]; then
|
||||
# Cleanup dangling images setelah redeploy sukses
|
||||
echo "🧹 Membersihkan dangling images..."
|
||||
curl -s -X POST "https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/images/prune" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"filters":{"dangling":["true"]}}' | jq -r '" Reclaimed: \(.SpaceReclaimed // 0 | . / 1073741824 | tostring | .[0:5]) GB"'
|
||||
|
||||
echo "✅ Cleanup selesai!"
|
||||
echo ""
|
||||
echo "✅ Stack $STACK_NAME berhasil di-redeploy dengan image baru dan running!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "❌ Timeout $((MAX_RETRY * SLEEP_INTERVAL))s! Container baru tidak kunjung running."
|
||||
echo " Kemungkinan image masih dalam proses pull atau ada error di server."
|
||||
exit 1
|
||||
55
.github/workflows/test.yml
vendored
55
.github/workflows/test.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: test workflows
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: "Target environment (e.g., staging, production)"
|
||||
required: true
|
||||
default: "staging"
|
||||
version:
|
||||
description: "Version to deploy"
|
||||
required: false
|
||||
default: "latest"
|
||||
|
||||
env:
|
||||
APP_NAME: desa-darmasaba-action
|
||||
WA_PHONE: "6289697338821,6289697338822"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout kode sumber
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Setup Bun
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
# Create log file
|
||||
- name: Create log file
|
||||
run: touch build.txt
|
||||
|
||||
# Step 1: Set BRANCH_NAME based on event type
|
||||
- name: Set BRANCH_NAME
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Step 2: Generate APP_VERSION dynamically
|
||||
- name: Set APP_VERSION
|
||||
run: echo "APP_VERSION=${{ github.sha }}---$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
|
||||
|
||||
# Step 3: Kirim notifikasi ke API build Start
|
||||
- name: Notify start build
|
||||
run: |
|
||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
||||
for PHONE in "${PHONES[@]}"; do
|
||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:start\nApp:${{ env.APP_NAME }}\nenv:${{ inputs.environment }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}'))")
|
||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
||||
done
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -29,7 +29,12 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env
|
||||
# env local files (keep .env.example)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# QC
|
||||
QC
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -47,9 +52,9 @@ next-env.d.ts
|
||||
# cache
|
||||
/cache
|
||||
|
||||
.github/
|
||||
|
||||
.env.*
|
||||
|
||||
*.tar.gz
|
||||
|
||||
# local scripts
|
||||
ai.sh
|
||||
|
||||
|
||||
13
.qwen/settings.json
Normal file
13
.qwen/settings.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright-mcp": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"playwright-mcp@latest"
|
||||
],
|
||||
"timeout": 60000
|
||||
}
|
||||
},
|
||||
"$version": 3
|
||||
}
|
||||
9
.qwen/settings.json.orig
Normal file
9
.qwen/settings.json.orig
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright-mcp": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "playwright-mcp@latest"],
|
||||
"timeout": 60000
|
||||
}
|
||||
}
|
||||
}
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": []
|
||||
}
|
||||
167
AGENTS.md
Normal file
167
AGENTS.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file contains essential information for agentic coding agents working in the desa-darmasaba repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Desa Darmasaba is a Next.js 15 application for village management services in Badung, Bali. It uses:
|
||||
- **Framework**: Next.js 15 with App Router
|
||||
- **Language**: TypeScript with strict mode
|
||||
- **Styling**: Mantine UI components with custom CSS
|
||||
- **Backend**: Elysia.js API server integrated with Next.js
|
||||
- **Database**: PostgreSQL with Prisma ORM
|
||||
- **State Management**: Jotai for global state
|
||||
- **Authentication**: JWT with iron-session
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
npm run dev
|
||||
|
||||
# Production build
|
||||
npm run build
|
||||
|
||||
# Start production server
|
||||
npm start
|
||||
|
||||
# Database seeding
|
||||
bun run prisma/seed.ts
|
||||
|
||||
# Linting (ESLint)
|
||||
npx eslint .
|
||||
|
||||
# Type checking
|
||||
npx tsc --noEmit
|
||||
|
||||
# Prisma operations
|
||||
npx prisma generate
|
||||
npx prisma db push
|
||||
npx prisma studio
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
Currently no test framework is configured. When adding tests:
|
||||
- Set up test scripts in package.json
|
||||
- Consider Jest or Vitest for unit testing
|
||||
- Use Playwright for E2E testing
|
||||
- Update this section with specific test commands
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Imports
|
||||
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
||||
- Group imports: external libraries first, then internal modules
|
||||
- Keep import statements organized and remove unused imports
|
||||
|
||||
```typescript
|
||||
// External libraries
|
||||
import { useState } from 'react'
|
||||
import { Button, Stack } from '@mantine/core'
|
||||
|
||||
// Internal modules
|
||||
import ApiFetch from '@/lib/api-fetch'
|
||||
import { MyComponent } from '@/components/my-component'
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
- Strict mode enabled (`"strict": true`)
|
||||
- Target: ES2017
|
||||
- Module resolution: bundler
|
||||
- Path alias: `@/*` maps to `./src/*`
|
||||
|
||||
### Naming Conventions
|
||||
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
|
||||
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
|
||||
- **Variables/Functions**: camelCase
|
||||
- **Constants**: UPPER_SNAKE_CASE
|
||||
- **Database Models**: PascalCase (Prisma convention)
|
||||
|
||||
### Error Handling
|
||||
- Use try-catch blocks for async operations
|
||||
- Implement proper error boundaries in React components
|
||||
- Log errors appropriately without exposing sensitive data
|
||||
- Use Zod for runtime validation and type safety
|
||||
|
||||
### API Structure
|
||||
- Backend uses Elysia.js with TypeScript
|
||||
- API routes are in `src/app/api/[[...slugs]]/` directory
|
||||
- Use treaty client for type-safe API calls
|
||||
- Follow RESTful conventions for endpoints
|
||||
- Include proper HTTP status codes and error responses
|
||||
|
||||
### Database Operations
|
||||
- Use Prisma client from `@/lib/prisma.ts`
|
||||
- Database connection includes graceful shutdown handling
|
||||
- Use transactions for complex operations
|
||||
- Implement proper error handling for database queries
|
||||
|
||||
### Component Guidelines
|
||||
- Use functional components with hooks
|
||||
- Implement proper prop types with TypeScript interfaces
|
||||
- Use Mantine components for UI consistency
|
||||
- Follow atomic design principles when possible
|
||||
- Add loading states and error states for async operations
|
||||
|
||||
### State Management
|
||||
- Use Jotai atoms for global state
|
||||
- Keep local state in components when possible
|
||||
- Use React Query (SWR) for server state caching
|
||||
- Implement optimistic updates for better UX
|
||||
|
||||
### Styling
|
||||
- Primary: Mantine UI components
|
||||
- Use Mantine theme system for customization
|
||||
- Custom CSS should be minimal and scoped
|
||||
- Follow responsive design principles
|
||||
- Use semantic HTML5 elements
|
||||
|
||||
### Environment Variables
|
||||
- Use `.env.local` for development
|
||||
- Prefix public variables with `NEXT_PUBLIC_`
|
||||
- Never commit environment files to version control
|
||||
- Use proper typing for environment variables
|
||||
|
||||
### File Organization
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js app router pages
|
||||
├── components/ # Reusable React components
|
||||
├── lib/ # Utility functions and configurations
|
||||
├── state/ # Jotai atoms and state management
|
||||
├── types/ # TypeScript type definitions
|
||||
└── con/ # Constants and static data
|
||||
```
|
||||
|
||||
### Security Practices
|
||||
- Validate all user inputs with Zod schemas
|
||||
- Use JWT tokens for authentication
|
||||
- Implement proper CORS configuration
|
||||
- Never expose database credentials or API keys
|
||||
- Use HTTPS in production
|
||||
- Implement rate limiting for sensitive endpoints
|
||||
|
||||
### Performance Considerations
|
||||
- Use Next.js Image optimization
|
||||
- Implement proper caching strategies
|
||||
- Use React.memo for expensive components
|
||||
- Optimize bundle size with dynamic imports
|
||||
- Use Prisma query optimization
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Always run type checking before committing: `npx tsc --noEmit`
|
||||
2. Run linting to catch style issues: `npx eslint .`
|
||||
3. Test database changes with `npx prisma db push`
|
||||
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
||||
5. Check environment variables are properly configured
|
||||
6. Verify responsive design on different screen sizes
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The application uses a custom Elysia.js server integrated with Next.js API routes
|
||||
- Image uploads are handled through `/api/upl-img-single` endpoint
|
||||
- Database seeding is done with Bun runtime
|
||||
- The app supports Indonesian locale (id_ID) for SEO and content
|
||||
- CORS is configured to allow cross-origin requests during development
|
||||
73
AUDIT_REPORT.md
Normal file
73
AUDIT_REPORT.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# Engineering Audit Report: Desa Darmasaba
|
||||
**Status:** Production Readiness Review (Critical)
|
||||
**Auditor:** Staff Technical Architect
|
||||
|
||||
---
|
||||
|
||||
## 📊 Executive Summary & Scores
|
||||
|
||||
| Category | Score | Status |
|
||||
| :--- | :---: | :--- |
|
||||
| **Project Architecture** | 3/10 | 🔴 Critical Failure |
|
||||
| **Code Quality** | 4/10 | 🟠 Poor |
|
||||
| **Performance** | 5/10 | 🟡 Mediocre |
|
||||
| **Security** | 5/10 | 🟠 Risk Detected |
|
||||
| **Production Readiness** | 2/10 | 🔴 Not Ready |
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 1. Project Architecture
|
||||
The project suffers from a **"Frankenstein Architecture"**. It attempts to run a full Elysia.js instance inside a Next.js Catch-All route.
|
||||
- **Fractured Backend:** Logic is split between standard Next.js routes (`/api/auth`) and embedded Elysia modules.
|
||||
- **Stateful Dependency:** Reliance on local filesystem (`WIBU_UPLOAD_DIR`) makes the application impossible to deploy on modern serverless platforms like Vercel.
|
||||
- **Polluted Namespace:** Routing tree contains "test/coba" folders (`src/app/coba`, `src/app/percobaan`) that would be accessible in production.
|
||||
|
||||
## ⚛️ 2. Frontend Engineering (React / Next.js)
|
||||
- **State Management Chaos:** Simultaneous use of `Valtio`, `Jotai`, `React Context`, and `localStorage`.
|
||||
- **Tight Coupling:** Public pages (`/darmasaba`) import state directly from Admin internal states (`/admin/(dashboard)/_state`).
|
||||
- **Heavy Client-Side Logic:** Logic that belongs in Server Actions or Hooks is embedded in presentational components (e.g., `Footer.tsx`).
|
||||
|
||||
## 📡 3. Backend / API Design
|
||||
- **Framework Overhead:** Running Elysia inside Next.js adds unnecessary cold-boot overhead and complexity.
|
||||
- **Weak Validation:** Widespread use of `as Type` casting in API handlers instead of runtime validation (Zod/Schema).
|
||||
- **Service Integration:** OTP codes are sent via external `GET` requests with sensitive data in the query string—a major logging risk.
|
||||
|
||||
## 🗄️ 4. Database & Data Modeling (Prisma)
|
||||
- **Schema Over-Normalization:** ~2000 lines of schema. Every minor content type (e.g., `LambangDesa`) is a separate table instead of a unified CMS model.
|
||||
- **Polymorphic Monolith:** `FileStorage` is a "god table" with optional relations to ~40 other tables, creating a massive bottleneck and data integrity risk.
|
||||
- **Connection Mismanagement:** Manual `prisma.$disconnect()` in API routes kills connection pooling performance.
|
||||
|
||||
## 🚀 5. Performance Engineering
|
||||
- **Bypassing Optimization:** Custom `/api/utils/img` endpoint bypasses `next/image` optimization, serving uncompressed assets.
|
||||
- **Aggressive Polling:** Client-side 30s polling for notifications is battery-draining and inefficient compared to SSE or SWR.
|
||||
|
||||
## 🔒 6. Security Audit
|
||||
- **Insecure OTP Delivery:** Credentials passed as URL parameters to the WhatsApp service.
|
||||
- **File Upload Risks:** Potential for Arbitrary File Upload due to direct local filesystem writes without rigorous sanitization.
|
||||
|
||||
## 🧹 7. Code Quality
|
||||
- **Inconsistency:** Mixed English/Indonesian naming (e.g., `nomor` vs `createdAt`).
|
||||
- **Artifacts:** Root directory is littered with scratch files: `xcoba.ts`, `xx.ts`, `test.txt`.
|
||||
|
||||
---
|
||||
|
||||
## 🚩 Top 10 Critical Problems
|
||||
1. **Architectural Fracture:** Embedding Elysia inside Next.js creates a "split-brain" system.
|
||||
2. **Serverless Incompatibility:** Dependency on local disk storage for uploads.
|
||||
3. **Database Bloat:** Over-complicated schema with a fragile `FileStorage` monolith.
|
||||
4. **State Fragmentation:** Mixed usage of Jotai and Valtio without a clear standard.
|
||||
5. **Credential Leakage:** OTP codes sent via GET query parameters.
|
||||
6. **Poor Cleanup:** Trial/Test folders and files committed to the production source.
|
||||
7. **Asset Performance:** Bypassing Next.js image optimization.
|
||||
8. **Coupling:** High dependency between public UI and internal Admin state.
|
||||
9. **Type Safety:** Manual casting in APIs instead of runtime validation.
|
||||
10. **Connection Pooling:** Inefficient Prisma connection management.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Lead Refactoring Priorities
|
||||
1. **Unify the API:** Decommission the Elysia wrapper. Port all logic to standard Next.js Route Handlers with Zod validation.
|
||||
2. **Stateless Storage:** Implement an S3-compatible adapter for all file uploads. Remove `fs` usage.
|
||||
3. **Schema Consolidation:** Refactor the schema to use generic content models where possible.
|
||||
4. **Standardize State:** Choose one global state manager and migrate all components.
|
||||
5. **Project Sanitization:** Delete all `coba`, `percobaan`, and scratch files (`xcoba.ts`, etc.).
|
||||
116
CLAUDE.md
Normal file
116
CLAUDE.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Desa Darmasaba is a full-stack digital village management platform for a village in Badung, Bali. It serves both a public-facing website (`/darmasaba/*`) and an admin CMS (`/admin/*`).
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Development
|
||||
bun run dev # Start dev server (port 3000)
|
||||
bun run build # Production build
|
||||
bun run tsc --noEmit # Type-check only
|
||||
|
||||
# Testing
|
||||
bun run test # All tests
|
||||
bun run test:api # Unit tests (Vitest)
|
||||
bun run test:e2e # E2E tests (Playwright)
|
||||
|
||||
# Database
|
||||
bunx prisma migrate deploy # Apply migrations
|
||||
bunx prisma migrate dev --name <name> # Create migration
|
||||
bun run prisma/seed.ts # Seed database
|
||||
bunx prisma studio # Interactive DB viewer
|
||||
|
||||
# Linting
|
||||
bun eslint . --fix
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Tech Stack
|
||||
- **Framework**: Next.js 15 (App Router) + React 19
|
||||
- **Runtime/Package manager**: Bun (not npm)
|
||||
- **API server**: Elysia.js (mounted at `/api/[[...slugs]]`)
|
||||
- **ORM**: Prisma + PostgreSQL
|
||||
- **UI**: Mantine UI v7-8
|
||||
- **State**: Jotai (atoms), Valtio (proxies), SWR (data fetching)
|
||||
- **Auth**: iron-session + JWT
|
||||
- **File storage**: Local uploads + Seafile (self-hosted)
|
||||
|
||||
### Request Flow
|
||||
|
||||
```
|
||||
Browser → Next.js middleware (src/middleware.ts)
|
||||
→ Public pages: src/app/darmasaba/
|
||||
→ Admin pages: src/app/admin/
|
||||
→ API: src/app/api/[[...slugs]]/route.ts (Elysia.js)
|
||||
└── _lib/*.ts (domain modules)
|
||||
```
|
||||
|
||||
The Elysia server is a single entry point with domain-specific modules: `desa.ts`, `kesehatan.ts`, `ekonomi.ts`, `keamanan.ts`, `lingkungan.ts`, `pendidikan.ts`, `kependudukan.ts`, `ppid.ts`, `inovasi.ts`, `auth/`, `user/`, `fileStorage/`. Swagger docs are auto-generated at `/api/docs`.
|
||||
|
||||
### Domain Modules
|
||||
Each domain (desa, kesehatan, ekonomi, etc.) has:
|
||||
- API handler in `src/app/api/[[...slugs]]/_lib/<domain>.ts`
|
||||
- Admin CMS pages in `src/app/admin/(dashboard)/<domain>/`
|
||||
- Public pages in `src/app/darmasaba/(pages)/<domain>/`
|
||||
|
||||
### Database (Prisma)
|
||||
- Schema at `prisma/schema.prisma` (~2400 lines, 100+ models)
|
||||
- Common model conventions: `@default(cuid())` IDs, `createdAt`/`updatedAt` timestamps, `deletedAt DateTime?` (soft delete), `isActive Boolean @default(true)`
|
||||
- Seeders per-module in `prisma/_seeder_list/`, orchestrated by `prisma/seed.ts`
|
||||
|
||||
### Authentication Flow
|
||||
1. User submits phone → OTP sent (email/SMS)
|
||||
2. OTP validated → JWT created + iron-session stored
|
||||
3. `UserSession` model tracks active sessions
|
||||
4. `src/middleware.ts` validates on each request
|
||||
5. `src/lib/api-auth.ts` handles JWT/session checks in API routes
|
||||
|
||||
### File Handling
|
||||
All uploaded files reference the `FileStorage` Prisma model. Uploads land in `WIBU_UPLOAD_DIR` (default: `uploads/`). Seafile is the external storage fallback.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/middleware.ts` | Route guards and auth |
|
||||
| `src/lib/prisma.ts` | Prisma client singleton |
|
||||
| `src/lib/api-auth.ts` | JWT/session validation |
|
||||
| `src/lib/api-fetch.ts` | Typed fetch wrapper used by frontend |
|
||||
| `src/lib/session.ts` | iron-session config |
|
||||
| `next.config.ts` | Next.js config (cache headers, allowed origins) |
|
||||
| `postcss.config.cjs` | Mantine CSS preset and breakpoints |
|
||||
| `docker-entrypoint.sh` | Runs `prisma migrate deploy` then starts app |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Copy `.env.example` to `.env`. Required variables:
|
||||
|
||||
```env
|
||||
DATABASE_URL="postgresql://..."
|
||||
NEXT_PUBLIC_BASE_URL="/"
|
||||
BASE_SESSION_KEY="..." # random string
|
||||
BASE_TOKEN_KEY="..." # random string
|
||||
SESSION_PASSWORD="..." # min 32 chars
|
||||
SEAFILE_TOKEN="..."
|
||||
SEAFILE_REPO_ID="..."
|
||||
SEAFILE_URL="..."
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
Multi-stage build: `oven/bun:1-debian` → builder → runner. The runner creates a `nextjs` user (UID 1001), exposes port 3000, and mounts `/app/uploads` as a volume. Entrypoint runs migrations automatically.
|
||||
|
||||
## CI/CD
|
||||
|
||||
GitHub Actions workflows in `.github/workflows/`:
|
||||
- `docker-publish.yml` — triggers on `v*` tags, pushes to GHCR
|
||||
- `publish.yml` — manual build & push
|
||||
- `re-pull.yml` — triggers Portainer to redeploy latest image
|
||||
|
||||
To release: tag with `git tag -a v0.1.x -m "..."` and push the tag.
|
||||
191
DEV-INSPECTOR-ANALYSIS.md
Normal file
191
DEV-INSPECTOR-ANALYSIS.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Dev Inspector - Analisis & Rekomendasi untuk Project Desa Darmasaba
|
||||
|
||||
## 📋 Ringkasan Analisis
|
||||
|
||||
Dokumen `dev-inspector-click-to-source.md` **TIDAK dapat diterapkan langsung** ke project ini karena perbedaan arsitektur fundamental.
|
||||
|
||||
## 🔍 Perbedaan Arsitektur
|
||||
|
||||
| Syarat di Dokumen | Project Desa Darmasaba | Status |
|
||||
|-------------------|------------------------|--------|
|
||||
| **Vite sebagai bundler** | Next.js 15 (Webpack/Turbopack) | ❌ Tidak kompatibel |
|
||||
| **Elysia + Vite middlewareMode** | Next.js App Router + Elysia sebagai API handler | ❌ Berbeda |
|
||||
| **React** | ✅ React 19 | ✅ Kompatibel |
|
||||
| **Bun runtime** | ✅ Bun | ✅ Kompatibel |
|
||||
|
||||
## ✅ Solusi: Next.js Sudah Punya Built-in Click-to-Source
|
||||
|
||||
Next.js memiliki fitur **click-to-source bawaan** yang bekerja tanpa setup tambahan:
|
||||
|
||||
### Cara Menggunakan
|
||||
|
||||
1. **Pastikan dalam development mode:**
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
2. **Klik elemen dengan modifier key:**
|
||||
- **macOS**: `Option` + `Click` (atau `⌥` + `Click`)
|
||||
- **Windows/Linux**: `Alt` + `Click`
|
||||
|
||||
3. **File akan terbuka di editor** pada baris dan kolom yang tepat
|
||||
|
||||
### Syarat Agar Berfungsi
|
||||
|
||||
1. **Editor harus ada di PATH**
|
||||
|
||||
VS Code biasanya sudah terdaftar. Jika menggunakan editor lain, set:
|
||||
```bash
|
||||
# Untuk Cursor
|
||||
export EDITOR=cursor
|
||||
|
||||
# Untuk Windsurf
|
||||
export EDITOR=windsurf
|
||||
|
||||
# Untuk Sublime Text
|
||||
export EDITOR=subl
|
||||
```
|
||||
|
||||
2. **Hanya berfungsi di development mode**
|
||||
- Fitur ini otomatis tree-shaken di production
|
||||
- Zero overhead di production build
|
||||
|
||||
3. **Browser DevTools harus terbuka** (beberapa browser memerlukan ini)
|
||||
|
||||
## 🎯 Rekomendasi untuk Project Ini
|
||||
|
||||
### Opsi 1: Gunakan Built-in Next.js (DIREKOMENDASIKAN)
|
||||
|
||||
**Kelebihan:**
|
||||
- ✅ Zero setup
|
||||
- ✅ Maintain oleh Vercel
|
||||
- ✅ Otomatis compatible dengan Next.js updates
|
||||
- ✅ Zero production overhead
|
||||
|
||||
**Kekurangan:**
|
||||
- ⚠️ Hotkey berbeda (`Option+Click` vs `Ctrl+Shift+Cmd+C`)
|
||||
- ⚠️ Tidak ada visual overlay/tooltip seperti di dokumen
|
||||
|
||||
**Cara:**
|
||||
Tidak perlu melakukan apapun - fitur sudah aktif saat `bun run dev`.
|
||||
|
||||
### Opsi 2: Custom Implementation (JIKA DIPERLUKAN)
|
||||
|
||||
Jika ingin visual overlay dan tooltip seperti di dokumen, bisa dibuat custom component dengan pendekatan berbeda:
|
||||
|
||||
#### Arsitektur Alternatif untuk Next.js
|
||||
|
||||
```
|
||||
BUILD TIME (Next.js/Webpack):
|
||||
.tsx/.jsx file
|
||||
→ [Custom Webpack Loader] inject data-inspector-* attributes
|
||||
→ [Next.js internal transform] JSX to React.createElement
|
||||
→ Browser menerima elemen dengan attributes
|
||||
|
||||
RUNTIME (Browser):
|
||||
[SAMA seperti dokumen - DevInspector component]
|
||||
|
||||
BACKEND (Next.js API Route):
|
||||
/__open-in-editor → Bun.spawn([editor, '--goto', 'file:line:col'])
|
||||
```
|
||||
|
||||
#### Komponen yang Dibutuhkan:
|
||||
|
||||
1. **Custom Webpack Loader** (bukan Vite Plugin)
|
||||
- Inject attributes via webpack transform
|
||||
- Taruh di `next.config.ts` webpack config
|
||||
|
||||
2. **DevInspector Component** (sama seperti dokumen)
|
||||
- Browser runtime untuk handle hotkey & klik
|
||||
|
||||
3. **API Route `/__open-in-editor`**
|
||||
- Buat sebagai Next.js API route: `src/app/api/__open-in-editor/route.ts`
|
||||
- HARUS bypass auth middleware
|
||||
|
||||
4. **Conditional Import** (sama seperti dokumen)
|
||||
```tsx
|
||||
const InspectorWrapper = process.env.NODE_ENV === 'development'
|
||||
? (await import('./DevInspector')).DevInspector
|
||||
: ({ children }) => <>{children}</>
|
||||
```
|
||||
|
||||
#### Implementasi Steps:
|
||||
|
||||
Jika Anda ingin melanjutkan dengan custom implementation, berikut steps:
|
||||
|
||||
1. ✅ Buat `src/components/DevInspector.tsx` (copy dari dokumen)
|
||||
2. ⚠️ Buat webpack loader untuk inject attributes (perlu research)
|
||||
3. ✅ Buat API route `src/app/api/__open-in-editor/route.ts`
|
||||
4. ✅ Wrap root layout dengan DevInspector
|
||||
5. ✅ Set `REACT_EDITOR` di `.env`
|
||||
|
||||
**Peringatan:**
|
||||
- Webpack loader lebih kompleks daripada Vite plugin
|
||||
- Mungkin ada edge cases dengan Next.js internals
|
||||
- Perlu maintenance ekstra saat Next.js update
|
||||
|
||||
## 📊 Perbandingan
|
||||
|
||||
| Fitur | Built-in Next.js | Custom Implementation |
|
||||
|-------|------------------|----------------------|
|
||||
| Setup | ✅ Zero | ⚠️ Medium |
|
||||
| Visual Overlay | ❌ Tidak ada | ✅ Ada |
|
||||
| Tooltip | ❌ Tidak ada | ✅ Ada |
|
||||
| Hotkey | `Option+Click` | Custom (bisa disesuaikan) |
|
||||
| Maintenance | ✅ Vercel | ⚠️ Manual |
|
||||
| Compatibility | ✅ Guaranteed | ⚠️ Perlu testing |
|
||||
| Production Impact | ✅ Zero | ✅ Zero (dengan conditional import) |
|
||||
|
||||
## 🎯 Kesimpulan
|
||||
|
||||
**Rekomendasi: Gunakan Built-in Next.js**
|
||||
|
||||
Alasan:
|
||||
1. ✅ Sudah tersedia - tidak perlu setup
|
||||
2. ✅ Lebih stabil - maintain oleh Vercel
|
||||
3. ✅ Lebih simple - tidak ada custom code
|
||||
4. ✅ Future-proof - otomatis update dengan Next.js
|
||||
|
||||
**Custom implementation hanya diperlukan jika:**
|
||||
- Anda sangat membutuhkan visual overlay & tooltip
|
||||
- Anda ingin hotkey yang sama persis (`Ctrl+Shift+Cmd+C`)
|
||||
- Anda punya waktu untuk maintenance
|
||||
|
||||
## 🚀 Quick Start - Built-in Feature
|
||||
|
||||
Untuk menggunakan click-to-source bawaan Next.js:
|
||||
|
||||
1. Jalankan development server:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
2. Buka browser ke `http://localhost:3000`
|
||||
|
||||
3. Tahan `Option` (macOS) atau `Alt` (Windows/Linux)
|
||||
|
||||
4. Cursor akan berubah menjadi crosshair
|
||||
|
||||
5. Klik elemen mana pun - file akan terbuka di editor
|
||||
|
||||
6. **Opsional**: Set editor di `.env`:
|
||||
```env
|
||||
# .env.local
|
||||
EDITOR=code # atau cursor, windsurf, subl
|
||||
```
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- Fitur ini hanya aktif di development mode (`NODE_ENV=development`)
|
||||
- Production build (`bun run build`) otomatis menghilangkan fitur ini
|
||||
- Next.js menggunakan mekanisme yang mirip (source mapping) untuk menentukan lokasi component
|
||||
- Jika editor tidak terbuka, pastikan:
|
||||
- Editor sudah terinstall dan ada di PATH
|
||||
- Browser DevTools terbuka (beberapa browser require ini)
|
||||
- Anda menggunakan development server, bukan production
|
||||
|
||||
## 🔗 Referensi
|
||||
|
||||
- [Next.js Documentation - Launching Editor](https://nextjs.org/docs/app/api-reference/config/next-config-js/reactStrictMode)
|
||||
- [React DevTools - Component Inspection](https://react.dev/learn/react-developer-tools)
|
||||
- [Original Dev Inspector Document](./dev-inspector-click-to-source.md)
|
||||
76
Dockerfile
Normal file
76
Dockerfile
Normal file
@@ -0,0 +1,76 @@
|
||||
# ==============================
|
||||
# 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
|
||||
|
||||
# Generate API types (opsional)
|
||||
RUN bun run gen:api || echo "tidak ada gen api"
|
||||
|
||||
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
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
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 --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/src/lib ./src/lib
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/tsconfig.json ./tsconfig.json
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/next.config.* ./
|
||||
COPY --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
# Create uploads directory with proper permissions
|
||||
RUN mkdir -p /app/uploads && chown nextjs:nodejs /app/uploads
|
||||
|
||||
USER nextjs
|
||||
|
||||
# Persistent storage for uploaded files
|
||||
VOLUME ["/app/uploads"]
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["/app/docker-entrypoint.sh"]
|
||||
62
GEMINI.md
Normal file
62
GEMINI.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Project: Desa Darmasaba
|
||||
|
||||
## Project Overview
|
||||
|
||||
The `desa-darmasaba` project is a Next.js (version 15+) application developed with TypeScript. It serves as an official platform for Desa Darmasaba (a village in Badung, Bali), offering various public services, news, and detailed village profiles.
|
||||
|
||||
**Key Technologies:**
|
||||
|
||||
* **Frontend Framework:** Next.js (v15+) with React (v19+)
|
||||
* **Language:** TypeScript
|
||||
* **UI Library:** Mantine UI
|
||||
* **Database ORM:** Prisma (v6+)
|
||||
* **Database:** PostgreSQL (as configured in `prisma/schema.prisma`)
|
||||
* **API Framework:** Elysia (used for API routes, as seen in dependencies)
|
||||
* **State Management:** Potentially Jotai and Valtio (listed in dependencies)
|
||||
* **Image Processing:** Sharp
|
||||
* **Package Manager:** Likely Bun, given `bun.lockb` and the `prisma:seed` script.
|
||||
|
||||
The application architecture follows the Next.js App Router structure, with comprehensive data models defined in `prisma/schema.prisma` covering various domains like public information, health, security, economy, innovation, environment, and education. It also includes configurations for image handling and caching.
|
||||
|
||||
## Building and Running
|
||||
|
||||
This project uses `bun` as the package manager. Ensure Bun is installed to run these commands.
|
||||
|
||||
* **Install Dependencies:**
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
* **Development Server:**
|
||||
Runs the Next.js development server.
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
* **Build for Production:**
|
||||
Builds the Next.js application for production deployment.
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
* **Start Production Server:**
|
||||
Starts the Next.js application in production mode.
|
||||
```bash
|
||||
bun run start
|
||||
```
|
||||
|
||||
* **Database Seeding:**
|
||||
Executes the Prisma seeding script to populate the database.
|
||||
```bash
|
||||
bun run prisma:seed
|
||||
```
|
||||
|
||||
## Development Conventions
|
||||
|
||||
* **Coding Language:** TypeScript is strictly enforced.
|
||||
* **Frontend Framework:** Next.js App Router for page and component structuring.
|
||||
* **UI/UX:** Adherence to Mantine UI component library for consistent styling and user experience.
|
||||
* **Database Interaction:** Prisma ORM is used for all database operations, with a PostgreSQL database.
|
||||
* **Linting:** ESLint is configured with `next/core-web-vitals` and `next/typescript` to maintain code quality and adherence to Next.js and TypeScript best practices.
|
||||
* **Styling:** PostCSS is used, with `postcss-preset-mantine` and `postcss-simple-vars` defining Mantine-specific breakpoints and other CSS variables.
|
||||
* **Imports:** Absolute imports are configured using `@/*` which resolves to the `src/` directory.
|
||||
34
MIND/PLAN/umkm-module.md
Normal file
34
MIND/PLAN/umkm-module.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Plan: UMKM Module Implementation
|
||||
|
||||
## Goal
|
||||
Implement UMKM, ProdukUmkm, and PenjualanProduk module with CRUD API and Dashboard analytics.
|
||||
|
||||
## Steps
|
||||
1. Update Prisma Schema (already done in file).
|
||||
2. Run database migration and seed data.
|
||||
3. Implement UMKM CRUD API.
|
||||
4. Implement ProdukUmkm CRUD API.
|
||||
5. Implement PenjualanProduk CRUD API.
|
||||
6. Implement Dashboard API (KPI, Summary, Top Produk, Detail Penjualan).
|
||||
7. Register all routers in the ekonomi module.
|
||||
8. Verify with type check and build.
|
||||
|
||||
## Progress
|
||||
- [x] Step 1: Update Prisma Schema
|
||||
- [x] Step 2: Run database migration
|
||||
- [x] Step 3: Implement UMKM CRUD API
|
||||
- [x] Step 4: Implement ProdukUmkm CRUD API
|
||||
- [x] Step 5: Implement PenjualanProduk CRUD API
|
||||
- [x] Step 6: Implement Dashboard API
|
||||
- [x] Step 7: Register routers
|
||||
- [x] Step 8: Verify changes
|
||||
- [x] Step 9: Implement Admin UI Layout and Tabs
|
||||
- [x] Step 10: Implement Dashboard UI Page
|
||||
- [x] Step 11: Implement Data UMKM UI Page
|
||||
- [x] Step 12: Implement Produk UI Page
|
||||
- [x] Step 13: Implement Penjualan UI Page
|
||||
- [x] Step 14: Register UI pages in Admin Menu
|
||||
- [x] Step 15: Implement Public UMKM Directory Page
|
||||
- [x] Step 16: Implement Public UMKM Detail Page
|
||||
- [x] Step 17: Implement Public Product Catalog Page
|
||||
- [x] Step 18: Register public pages in Navbar
|
||||
37
MIND/SUMMARY/umkm-module-summary.md
Normal file
37
MIND/SUMMARY/umkm-module-summary.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Summary: UMKM Module Implementation
|
||||
|
||||
## Accomplishments
|
||||
- Successfully migrated the database to include `Umkm`, `ProdukUmkm`, and `PenjualanProduk` tables.
|
||||
- Implemented a complete set of CRUD API endpoints for UMKM, Products, and Sales.
|
||||
- Developed a comprehensive Dashboard API providing KPIs, sales summaries, top products, and detailed stock analytics.
|
||||
- Integrated the new module into the existing `ekonomi` router.
|
||||
- Implemented the Admin UI with a modern tab-based layout.
|
||||
- Created four main admin pages: Dashboard, Data UMKM, Produk, and Penjualan.
|
||||
- Registered the new UMKM module in the Admin Navigation Menu for all roles.
|
||||
- Implemented the Public UI for citizens to browse local businesses.
|
||||
- Created three public pages: Direktori UMKM, UMKM Detail, and Katalog Produk.
|
||||
- Registered the public UMKM pages in the main Website Navbar under the Ekonomi section.
|
||||
- Verified the implementation with `tsc` and `bun run build`.
|
||||
|
||||
## Files Created/Modified
|
||||
### Modified
|
||||
- `prisma/schema.prisma`: Added relations and models.
|
||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/index.ts`: Registered new routers.
|
||||
- `src/app/admin/_com/list_PageAdmin.tsx`: Registered new UI pages in menu.
|
||||
|
||||
### Created
|
||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/`: CRUD for UMKM.
|
||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/produk/`: CRUD for Products.
|
||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/penjualan/`: CRUD for Sales with stock management.
|
||||
- `src/app/api/[[...slugs]]/_lib/ekonomi/umkm/dashboard/`: Analytics endpoints.
|
||||
- `src/app/admin/(dashboard)/ekonomi/umkm/`: Admin UI pages and layouts.
|
||||
- `src/app/admin/(dashboard)/_state/ekonomi/umkm/umkm.ts`: Valtio state for the UMKM module.
|
||||
|
||||
## Stock Management Logic
|
||||
- Creating a sale decrements product stock.
|
||||
- Updating a sale adjusts stock based on the difference in quantity.
|
||||
- Deleting a sale increments stock back.
|
||||
|
||||
## Next Steps
|
||||
- Implement frontend UI for the UMKM module.
|
||||
- Add more comprehensive tests for the stock management logic.
|
||||
173
MUSIK_CREATE_ANALYSIS.md
Normal file
173
MUSIK_CREATE_ANALYSIS.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Musik Desa - Create Feature Analysis
|
||||
|
||||
## Error Summary
|
||||
**Error**: `ERR_BLOCKED_BY_CLIENT` saat create musik di staging environment
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### 1. **CORS Configuration Issue** (Primary)
|
||||
File: `src/app/api/[[...slugs]]/route.ts`
|
||||
|
||||
The CORS configuration has specific origins listed:
|
||||
```typescript
|
||||
const corsConfig = {
|
||||
origin: [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"https://cld-dkr-desa-darmasaba-stg.wibudev.com",
|
||||
"https://cld-dkr-staging-desa-darmasaba.wibudev.com",
|
||||
"*",
|
||||
],
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Problem**: The wildcard `*` is at the end, but some browsers don't respect it when `credentials: true` is set.
|
||||
|
||||
### 2. **API Fetch Base URL** (Secondary)
|
||||
File: `src/lib/api-fetch.ts`
|
||||
|
||||
```typescript
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'
|
||||
```
|
||||
|
||||
**Problem**:
|
||||
- In staging, this might still default to `http://localhost:3000`
|
||||
- Mixed content (HTTPS frontend → HTTP API) gets blocked by browsers
|
||||
- The `NEXT_PUBLIC_BASE_URL` environment variable might not be set in staging
|
||||
|
||||
### 3. **File Storage Upload Path** (Tertiary)
|
||||
File: `src/app/api/[[...slugs]]/_lib/fileStorage/_lib/create.ts`
|
||||
|
||||
```typescript
|
||||
const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR;
|
||||
```
|
||||
|
||||
**Problem**: If `WIBU_UPLOAD_DIR` is not set or points to a non-writable location, uploads will fail silently.
|
||||
|
||||
## Solution
|
||||
|
||||
### Fix 1: Update CORS Configuration
|
||||
**File**: `src/app/api/[[...slugs]]/route.ts`
|
||||
|
||||
```typescript
|
||||
// Move wildcard to first position and ensure it works with credentials
|
||||
const corsConfig = {
|
||||
origin: [
|
||||
"*", // Allow all origins (for staging flexibility)
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"https://cld-dkr-desa-darmasaba-stg.wibudev.com",
|
||||
"https://cld-dkr-staging-desa-darmasaba.wibudev.com",
|
||||
"https://desa-darmasaba-stg.wibudev.com"
|
||||
],
|
||||
methods: ["GET", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"] as HTTPMethod[],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "Accept"],
|
||||
exposedHeaders: ["Content-Range", "X-Content-Range"],
|
||||
maxAge: 86400, // 24 hours
|
||||
credentials: true,
|
||||
};
|
||||
```
|
||||
|
||||
### Fix 2: Add Environment Variable Validation
|
||||
**File**: `.env.example` (update)
|
||||
|
||||
```bash
|
||||
# Application Configuration
|
||||
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
|
||||
# For staging/production, set this to your actual domain
|
||||
# NEXT_PUBLIC_BASE_URL=https://cld-dkr-desa-darmasaba-stg.wibudev.com
|
||||
```
|
||||
|
||||
### Fix 3: Update API Fetch to Handle Relative URLs
|
||||
**File**: `src/lib/api-fetch.ts`
|
||||
|
||||
```typescript
|
||||
import { AppServer } from '@/app/api/[[...slugs]]/route'
|
||||
import { treaty } from '@elysiajs/eden'
|
||||
|
||||
// Use relative URL for better deployment flexibility
|
||||
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || '/'
|
||||
|
||||
const ApiFetch = treaty<AppServer>(BASE_URL)
|
||||
|
||||
export default ApiFetch
|
||||
```
|
||||
|
||||
### Fix 4: Add Error Handling in Create Page
|
||||
**File**: `src/app/admin/(dashboard)/musik/create/page.tsx`
|
||||
|
||||
Add better error logging to diagnose issues:
|
||||
|
||||
```typescript
|
||||
const handleSubmit = async () => {
|
||||
// ... validation ...
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Upload cover image
|
||||
const coverRes = await ApiFetch.api.fileStorage.create.post({
|
||||
file: coverFile,
|
||||
name: coverFile.name,
|
||||
});
|
||||
|
||||
if (!coverRes.data?.data?.id) {
|
||||
console.error('Cover upload failed:', coverRes);
|
||||
return toast.error('Gagal mengunggah cover, silakan coba lagi');
|
||||
}
|
||||
|
||||
// ... rest of the code ...
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating musik:', {
|
||||
error,
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
toast.error('Terjadi kesalahan saat membuat musik');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### Local Development
|
||||
- [ ] Test create musik with cover image and audio file
|
||||
- [ ] Verify CORS headers in browser DevTools Network tab
|
||||
- [ ] Check that file uploads are saved to correct directory
|
||||
|
||||
### Staging Environment
|
||||
- [ ] Set `NEXT_PUBLIC_BASE_URL` to staging domain
|
||||
- [ ] Verify HTTPS is used for all API calls
|
||||
- [ ] Check browser console for mixed content warnings
|
||||
- [ ] Verify `WIBU_UPLOAD_DIR` is set and writable
|
||||
- [ ] Test create musik end-to-end
|
||||
|
||||
## Additional Notes
|
||||
|
||||
### ERR_BLOCKED_BY_CLIENT Common Causes:
|
||||
1. **CORS policy blocking** - Most likely cause
|
||||
2. **Ad blockers** - Can block certain API endpoints
|
||||
3. **Mixed content** - HTTPS page making HTTP requests
|
||||
4. **Content Security Policy (CSP)** - Restrictive CSP headers
|
||||
5. **Browser extensions** - Privacy/security extensions blocking requests
|
||||
|
||||
### Debugging Steps:
|
||||
1. Open browser DevTools → Network tab
|
||||
2. Try to create musik
|
||||
3. Look for failed requests (red status)
|
||||
4. Check the "Headers" tab for:
|
||||
- Request URL (should be correct domain)
|
||||
- Response headers (should have `Access-Control-Allow-Origin`)
|
||||
- Status code (4xx/5xx indicates server-side issue)
|
||||
5. Check browser console for CORS errors
|
||||
|
||||
## Recommended Next Steps
|
||||
|
||||
1. **Immediate**: Update CORS configuration to allow staging domain
|
||||
2. **Short-term**: Add proper environment variable validation
|
||||
3. **Long-term**: Implement proper error boundaries and logging
|
||||
269
QWEN.md
Normal file
269
QWEN.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Desa Darmasaba - Village Management System
|
||||
|
||||
## Project Overview
|
||||
|
||||
Desa Darmasaba is a comprehensive Next.js 15 application designed for village management services in Darmasaba, Badung, Bali. The application serves as a digital platform for government services, public information, and community engagement. It features multiple sections including PPID (Public Information Disclosure), health services, security, education, environment, economy, innovation, and more.
|
||||
|
||||
### Key Technologies
|
||||
- **Framework**: Next.js 15 with App Router
|
||||
- **Language**: TypeScript with strict mode
|
||||
- **Styling**: Mantine UI components with custom CSS
|
||||
- **Backend**: Elysia.js API server integrated with Next.js
|
||||
- **Database**: PostgreSQL with Prisma ORM
|
||||
- **State Management**: Valtio for global state
|
||||
- **Authentication**: JWT with iron-session
|
||||
|
||||
### Architecture
|
||||
The application follows a modular architecture with:
|
||||
- A main frontend built with Next.js and Mantine UI
|
||||
- An integrated Elysia.js API server for backend operations
|
||||
- Prisma ORM for database interactions
|
||||
- File storage integration with Seafile
|
||||
- Multiple domain-specific modules (PPID, health, security, education, etc.)
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (with Bun runtime)
|
||||
- PostgreSQL database
|
||||
- Seafile server for file storage
|
||||
|
||||
### Setup Instructions
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
2. Set up environment variables in `.env.local`:
|
||||
```
|
||||
DATABASE_URL=your_postgresql_connection_string
|
||||
SEAFILE_TOKEN=your_seafile_token
|
||||
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||
SEAFILE_BASE_URL=your_seafile_base_url
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||
SEAFILE_URL=your_seafile_api_url
|
||||
WIBU_UPLOAD_DIR=your_upload_directory
|
||||
```
|
||||
|
||||
3. Generate Prisma client:
|
||||
```bash
|
||||
bunx prisma generate
|
||||
```
|
||||
|
||||
4. Push database schema:
|
||||
```bash
|
||||
bunx prisma db push
|
||||
```
|
||||
|
||||
5. Seed the database:
|
||||
```bash
|
||||
bun run prisma/seed.ts
|
||||
```
|
||||
|
||||
6. Run the development server:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### Available Scripts
|
||||
- `bun run dev` - Start development server
|
||||
- `bun run build` - Build for production
|
||||
- `bun run start` - Start production server
|
||||
- `bun run prisma/seed.ts` - Run database seeding
|
||||
- `bunx prisma generate` - Generate Prisma client
|
||||
- `bunx prisma db push` - Push schema changes to database
|
||||
- `bunx prisma studio` - Open Prisma Studio GUI
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Code Structure
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js app router pages
|
||||
│ ├── admin/ # Admin dashboard pages
|
||||
│ ├── api/ # API routes with Elysia.js
|
||||
│ ├── darmasaba/ # Public-facing village pages
|
||||
│ └── ...
|
||||
├── con/ # Constants and configuration
|
||||
├── hooks/ # React hooks
|
||||
├── lib/ # Utility functions and configurations
|
||||
├── middlewares/ # Next.js middleware
|
||||
├── state/ # Global state management
|
||||
├── store/ # Additional state management
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
### Import Conventions
|
||||
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
||||
- Group imports: external libraries first, then internal modules
|
||||
- Keep import statements organized and remove unused imports
|
||||
|
||||
```typescript
|
||||
// External libraries
|
||||
import { useState } from 'react'
|
||||
import { Button, Stack } from '@mantine/core'
|
||||
|
||||
// Internal modules
|
||||
import ApiFetch from '@/lib/api-fetch'
|
||||
import { MyComponent } from '@/components/my-component'
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
- Strict mode enabled (`"strict": true`)
|
||||
- Target: ES2017
|
||||
- Module resolution: bundler
|
||||
- Path alias: `@/*` maps to `./src/*`
|
||||
|
||||
### Naming Conventions
|
||||
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
|
||||
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
|
||||
- **Variables/Functions**: camelCase
|
||||
- **Constants**: UPPER_SNAKE_CASE
|
||||
- **Database Models**: PascalCase (Prisma convention)
|
||||
|
||||
### Error Handling
|
||||
- Use try-catch blocks for async operations
|
||||
- Implement proper error boundaries in React components
|
||||
- Log errors appropriately without exposing sensitive data
|
||||
- Use Zod for runtime validation and type safety
|
||||
|
||||
### API Structure
|
||||
- Backend uses Elysia.js with TypeScript
|
||||
- API routes are in `src/app/api/[[...slugs]]/` directory
|
||||
- Use treaty client for type-safe API calls
|
||||
- Follow RESTful conventions for endpoints
|
||||
- Include proper HTTP status codes and error responses
|
||||
|
||||
### Database Operations
|
||||
- Use Prisma client from `@/lib/prisma.ts`
|
||||
- Database connection includes graceful shutdown handling
|
||||
- Use transactions for complex operations
|
||||
- Implement proper error handling for database queries
|
||||
|
||||
### Component Guidelines
|
||||
- Use functional components with hooks
|
||||
- Implement proper prop types with TypeScript interfaces
|
||||
- Use Mantine components for UI consistency
|
||||
- Follow atomic design principles when possible
|
||||
- Add loading states and error states for async operations
|
||||
|
||||
### State Management
|
||||
- Use Valtio proxies for global state
|
||||
- Keep local state in components when possible
|
||||
- Use SWR for server state caching
|
||||
- Implement optimistic updates for better UX
|
||||
|
||||
### Styling
|
||||
- Primary: Mantine UI components
|
||||
- Use Mantine theme system for customization
|
||||
- Custom CSS should be minimal and scoped
|
||||
- Follow responsive design principles
|
||||
- Use semantic HTML5 elements
|
||||
|
||||
### Security Practices
|
||||
- Validate all user inputs with Zod schemas
|
||||
- Use JWT tokens for authentication
|
||||
- Implement proper CORS configuration
|
||||
- Never expose database credentials or API keys
|
||||
- Use HTTPS in production
|
||||
- Implement rate limiting for sensitive endpoints
|
||||
|
||||
### Performance Considerations
|
||||
- Use Next.js Image optimization
|
||||
- Implement proper caching strategies
|
||||
- Use React.memo for expensive components
|
||||
- Optimize bundle size with dynamic imports
|
||||
- Use Prisma query optimization
|
||||
|
||||
## Domain Modules
|
||||
|
||||
The application is organized into several domain modules:
|
||||
|
||||
1. **PPID (Public Information Disclosure)**: Profile, structure, information requests, legal basis
|
||||
2. **Health**: Health facilities, programs, emergency response, disease information
|
||||
3. **Security**: Community security, emergency contacts, crime prevention
|
||||
4. **Education**: Schools, scholarships, educational programs
|
||||
5. **Economy**: Local markets, BUMDes, employment data
|
||||
6. **Environment**: Environmental data, conservation, waste management
|
||||
7. **Innovation**: Digital services, innovation programs
|
||||
8. **Culture**: Village traditions, music, cultural preservation
|
||||
|
||||
Each module has its own section in both the admin panel and public-facing areas.
|
||||
|
||||
## File Storage Integration
|
||||
|
||||
The application integrates with Seafile for file storage, with specific handling for:
|
||||
- Images and documents
|
||||
- Public sharing capabilities
|
||||
- CDN URL generation
|
||||
- Batch processing of assets
|
||||
|
||||
## Testing
|
||||
|
||||
Currently no formal test framework is configured. When adding tests:
|
||||
- Consider Jest or Vitest for unit testing
|
||||
- Use Playwright for E2E testing
|
||||
- Update this section with specific test commands
|
||||
|
||||
## Deployment
|
||||
|
||||
The application includes deployment scripts in the `NOTE.md` file that outline:
|
||||
- Automated deployment with GitHub API integration
|
||||
- Environment-specific configurations
|
||||
- PM2 process management
|
||||
- Release management with versioning
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
- **API endpoints returning 404**: Check that environment variables are properly configured
|
||||
- **Database connection errors**: Verify DATABASE_URL in environment variables
|
||||
- **File upload issues**: Ensure Seafile integration is properly configured
|
||||
- **Build failures**: Run `bunx prisma generate` before building
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Always run type checking before committing: `bunx tsc --noEmit`
|
||||
2. Run linting to catch style issues: `bun run eslint .`
|
||||
3. Test database changes with `bunx prisma db push`
|
||||
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
||||
5. Check environment variables are properly configured
|
||||
6. Verify responsive design on different screen sizes
|
||||
|
||||
## Qwen Added Memories
|
||||
- **GitHub Workflow Execution**: Project ini memiliki 3 workflow GitHub Action:
|
||||
1. `publish.yml` - Build & push Docker image ke GHCR (manual trigger, butuh input: stack_env + tag)
|
||||
2. `re-pull.yml` - Re-pull Docker image di Portainer (manual trigger, butuh input: stack_name + stack_env)
|
||||
3. `docker-publish.yml` - Auto build & push saat ada tag versi v*
|
||||
|
||||
Workflow bisa dijalankan via GitHub CLI: `gh workflow run <nama.yml> -f param=value --ref branch`
|
||||
|
||||
Setelah commit ke branch deployment (dev/stg/prod), otomatis trigger workflow publish + re-pull untuk deploy ke server.
|
||||
|
||||
- **Deployment Workflow Sistematis**:
|
||||
1. **Version Bump** - Update `version` di `package.json` sebelum deploy (ikuti semver: major.minor.patch)
|
||||
2. **Commit** - Commit perubahan + version bump dengan pesan yang jelas
|
||||
3. **Push ke Branch** - Push ke branch target (biasanya `stg` untuk staging atau `prod` untuk production)
|
||||
4. **Trigger publish.yml** - Gunakan GitHub API atau CLI dengan: `ref: main`, `stack_env: stg`, `tag: <versi-dari-package.json>`
|
||||
5. **Tunggu publish selesai** - Workflow harus completed baru lanjut ke re-pull
|
||||
6. **Trigger re-pull.yml** - Gunakan GitHub API atau CLI dengan: `ref: main`, `stack_name: desa-darmasaba`, `stack_env: stg`
|
||||
|
||||
Branch deployment: `stg` (staging) atau `prod` (production)
|
||||
Version format di package.json: `"version": "major.minor.patch"`
|
||||
|
||||
- **Deployment Workflow HARUS Sequential (Berurutan)**:
|
||||
|
||||
Saat deploy ke stg atau prod, workflow TIDAK BOLEH dijalankan bersamaan. Harus menunggu yang pertama SELESAI total baru trigger yang kedua.
|
||||
|
||||
**Urutan yang BENAR:**
|
||||
1. ✅ **publish.yml** - Tunggu sampai SELESAI (status: ✓ success)
|
||||
2. ✅ **Setelah publish selesai**, baru trigger **re-pull.yml**
|
||||
|
||||
**JANGAN trigger keduanya bersamaan!** Ini akan menyebabkan race condition karena re-pull akan menarik image yang belum selesai di-build.
|
||||
|
||||
**Cara cek workflow selesai via GitHub CLI:**
|
||||
```bash
|
||||
gh run watch <publish_run_id>
|
||||
# Tunggu sampai ada checkmark ✓
|
||||
```
|
||||
678
STRUKTUR-PROJEK.md
Normal file
678
STRUKTUR-PROJEK.md
Normal file
@@ -0,0 +1,678 @@
|
||||
# Dokumentasi Struktur Proyek - Desa Darmasaba
|
||||
|
||||
## 1. Ringkasan Proyek
|
||||
|
||||
**Desa Darmasaba** adalah aplikasi web komprehensif untuk layanan pemerintahan desa di Desa Darmasaba, Kabupaten Badung, Bali. Aplikasi ini berfungsi sebagai platform digital untuk layanan pemerintah, informasi publik, dan keterlibatan masyarakat.
|
||||
|
||||
### Tech Stack
|
||||
|
||||
| Kategori | Teknologi |
|
||||
|----------|-----------|
|
||||
| **Framework Frontend** | Next.js 15 dengan App Router |
|
||||
| **Bahasa** | TypeScript (strict mode) |
|
||||
| **Styling** | Mantine UI v7/v8 + Custom CSS |
|
||||
| **Backend API** | Elysia.js (high-performance TypeScript framework) |
|
||||
| **Database** | PostgreSQL |
|
||||
| **ORM** | Prisma 6.3.1 |
|
||||
| **Runtime** | Bun |
|
||||
| **State Management** | Jotai + Valtio + SWR |
|
||||
| **Autentikasi** | iron-session + JWT |
|
||||
| **File Storage** | Seafile |
|
||||
| **Rich Text Editor** | TipTap |
|
||||
| **Charts** | Recharts + Chart.js |
|
||||
| **Maps** | Leaflet + react-leaflet |
|
||||
| **UI Components** | Mantine, PrimeReact, Framer Motion |
|
||||
| **Validasi** | Zod |
|
||||
| **Testing** | Vitest (unit), Playwright (E2E) |
|
||||
| **Deployment** | Docker + GitHub Actions + Portainer |
|
||||
| **Registry** | GitHub Container Registry (GHCR) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Struktur Direktori
|
||||
|
||||
```
|
||||
desa-darmasaba/
|
||||
├── .github/workflows/ # GitHub Actions CI/CD
|
||||
│ ├── docker-publish.yml # Auto build & push saat tag v*
|
||||
│ ├── publish.yml # Manual build & push ke GHCR
|
||||
│ ├── re-pull.yml # Manual re-pull image di Portainer
|
||||
│ └── script/ # Script deployment
|
||||
│
|
||||
├── prisma/
|
||||
│ ├── schema.prisma # Database schema (2413 baris, 100+ model)
|
||||
│ ├── seed.ts # Database seeder utama
|
||||
│ └── _seeder_list/ # Data seed per modul
|
||||
│ ├── desa/ # Seed berita, gallery, layanan, dll
|
||||
│ ├── ekonomi/ # Seed APBDes, demografi, dll
|
||||
│ ├── inovasi/ # Seed ide inovatif, desa digital
|
||||
│ ├── keamanan/ # Seed keamanan, kontak darurat
|
||||
│ ├── kesehatan/ # Seed fasilitas kesehatan, posyandu
|
||||
│ ├── kependudukan/ # Seed data penduduk
|
||||
│ ├── lingkungan/ # Seed lingkungan desa
|
||||
│ ├── pendidikan/ # Seed sekolah, beasiswa
|
||||
│ ├── ppid/ # Seed PPID
|
||||
│ └── landing-page/ # Seed landing page
|
||||
│
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router
|
||||
│ │ ├── _com/ # Komponen global (SplashScreen, WebVitals)
|
||||
│ │ ├── admin/ # Panel administrasi (protected)
|
||||
│ │ │ ├── _com/ # Komponen admin shared
|
||||
│ │ │ ├── (dashboard)/ # Dashboard admin dengan route groups
|
||||
│ │ │ │ ├── _com/ # Komponen dashboard shared
|
||||
│ │ │ │ ├── _state/ # State khusus dashboard
|
||||
│ │ │ │ ├── _utils/ # Utilitas dashboard
|
||||
│ │ │ │ ├── auth/ # Autentikasi admin
|
||||
│ │ │ │ ├── desa/ # Admin: berita, gallery, profil, layanan
|
||||
│ │ │ │ ├── ekonomi/ # Admin: APBDes, demografi, BUMDes
|
||||
│ │ │ │ ├── inovasi/ # Admin: ide inovatif, desa digital
|
||||
│ │ │ │ ├── keamanan/ # Admin: keamanan, kontak darurat
|
||||
│ │ │ │ ├── kependudukan/# Admin: banjar, agama, umur, migrasi
|
||||
│ │ │ │ ├── kesehatan/ # Admin: puskesmas, posyandu, wabah
|
||||
│ │ │ │ ├── landing-page/# Admin: konten landing page
|
||||
│ │ │ │ ├── lingkungan/ # Admin: konservasi, sampah, penghijauan
|
||||
│ │ │ │ ├── musik/ # Admin: musik desa
|
||||
│ │ │ │ ├── pendidikan/ # Admin: sekolah, beasiswa, perpustakaan
|
||||
│ │ │ │ ├── ppid/ # Admin: PPID, IKM, permohonan
|
||||
│ │ │ │ └── user&role/ # Admin: manajemen user & role
|
||||
│ │ │ ├── auth/ # Halaman login admin
|
||||
│ │ │ ├── csv/ # Upload/demo CSV
|
||||
│ │ │ ├── images/ # Manajemen gambar
|
||||
│ │ │ └── upload-demo/ # Demo upload
|
||||
│ │ │
|
||||
│ │ ├── api/ # API routes (Elysia.js)
|
||||
│ │ │ ├── [[...slugs]]/ # Catch-all route untuk Elysia
|
||||
│ │ │ │ ├── _lib/ # Modul API per domain
|
||||
│ │ │ │ │ ├── auth/ # Autentikasi API
|
||||
│ │ │ │ │ ├── desa/ # API modul desa
|
||||
│ │ │ │ │ ├── ekonomi/ # API modul ekonomi
|
||||
│ │ │ │ │ ├── fileStorage/ # API file storage
|
||||
│ │ │ │ │ ├── inovasi/ # API modul inovasi
|
||||
│ │ │ │ │ ├── keamanan/# API modul keamanan
|
||||
│ │ │ │ │ ├── kependudukan/ # API modul kependudukan
|
||||
│ │ │ │ │ ├── kesehatan/ # API modul kesehatan
|
||||
│ │ │ │ │ ├── landing_page/ # API landing page
|
||||
│ │ │ │ │ ├── lingkungan/ # API modul lingkungan
|
||||
│ │ │ │ │ ├── pendidikan/ # API modul pendidikan
|
||||
│ │ │ │ │ ├── ppid/ # API modul PPID
|
||||
│ │ │ │ │ ├── search/ # API pencarian global
|
||||
│ │ │ │ │ └── user/ # API user management
|
||||
│ │ │ │ └── route.ts # Entry point Elysia server
|
||||
│ │ │ ├── admin/ # API khusus admin
|
||||
│ │ │ ├── auth/ # API autentikasi
|
||||
│ │ │ ├── health/ # Health check endpoint
|
||||
│ │ │ ├── layout/ # API layout
|
||||
│ │ │ ├── news/ # API berita
|
||||
│ │ │ ├── subscribe/ # API subscription (email)
|
||||
│ │ │ └── tts/ # Text-to-Speech (ElevenLabs)
|
||||
│ │ │
|
||||
│ │ ├── context/ # React contexts
|
||||
│ │ │ └── MusicContext.tsx # Context untuk pemutar musik
|
||||
│ │ │
|
||||
│ │ ├── darmasaba/ # Halaman publik (front-facing)
|
||||
│ │ │ ├── _com/ # Komponen shared publik
|
||||
│ │ │ │ ├── main-page/ # Komponen halaman utama
|
||||
│ │ │ │ ├── Navbar.tsx # Navigasi utama
|
||||
│ │ │ │ ├── Footer.tsx # Footer
|
||||
│ │ │ │ ├── FixedPlayerBar.tsx # Music player bar
|
||||
│ │ │ │ ├── LoadDataFirstClient.tsx # Data prefetching
|
||||
│ │ │ │ ├── NewsReader.tsx # Component pembaca berita
|
||||
│ │ │ │ ├── globalSearch.tsx # Pencarian global
|
||||
│ │ │ │ └── scrollToTopButton.tsx
|
||||
│ │ │ ├── (pages)/ # Halaman publik utama
|
||||
│ │ │ │ ├── desa/ # Halaman: profil, berita, gallery, layanan
|
||||
│ │ │ │ ├── ekonomi/ # Halaman: APBDes, BUMDes, demografi
|
||||
│ │ │ │ ├── inovasi/ # Halaman: inovasi desa
|
||||
│ │ │ │ ├── keamanan/ # Halaman: keamanan lingkungan
|
||||
│ │ │ │ ├── kependudukan/# Halaman: data penduduk
|
||||
│ │ │ │ ├── kesehatan/ # Halaman: fasilitas kesehatan
|
||||
│ │ │ │ ├── lingkungan/ # Halaman: lingkungan desa
|
||||
│ │ │ │ ├── module/ # Halaman modul tambahan
|
||||
│ │ │ │ ├── musik/ # Halaman: musik desa
|
||||
│ │ │ │ ├── pendidikan/ # Halaman: pendidikan
|
||||
│ │ │ │ └── ppid/ # Halaman: PPID publik
|
||||
│ │ │ ├── (tambahan)/ # Halaman tambahan
|
||||
│ │ │ ├── layout.tsx # Layout utama publik
|
||||
│ │ │ └── page.tsx # Landing page utama
|
||||
│ │ │
|
||||
│ │ ├── login/ # Halaman login
|
||||
│ │ ├── registrasi/ # Halaman registrasi
|
||||
│ │ ├── waiting-room/ # Halaman waiting room
|
||||
│ │ ├── terms-of-service/ # Halaman syarat layanan
|
||||
│ │ ├── test-upload/ # Halaman tes upload
|
||||
│ │ ├── validasi/ # Halaman validasi
|
||||
│ │ ├── coba/ # Halaman percobaan
|
||||
│ │ ├── percobaan/ # Halaman percobaan lainnya
|
||||
│ │ ├── layout.tsx # Root layout (MantineProvider)
|
||||
│ │ ├── page.tsx # Root page
|
||||
│ │ ├── error.tsx # Error boundary
|
||||
│ │ ├── not-found.tsx # 404 page
|
||||
│ │ ├── globals.css # Global styles
|
||||
│ │ └── favicon.ico
|
||||
│ │
|
||||
│ ├── components/
|
||||
│ │ └── admin/ # Komponen admin reusable
|
||||
│ │ ├── AdminThemeProvider.tsx
|
||||
│ │ ├── DarkModeToggle.tsx
|
||||
│ │ ├── UnifiedSurface.tsx
|
||||
│ │ └── UnifiedTypography.tsx
|
||||
│ │
|
||||
│ ├── con/ # Constants & konfigurasi
|
||||
│ │ └── colors.ts # Palet warna
|
||||
│ │
|
||||
│ ├── lib/ # Utility functions
|
||||
│ │ ├── router/ # Router utilities
|
||||
│ │ ├── api-auth.ts # Autentikasi API
|
||||
│ │ ├── api-fetch.ts # Helper fetch API
|
||||
│ │ ├── EnvStringParse.ts # Parser environment variables
|
||||
│ │ ├── prisma.ts # Prisma client instance
|
||||
│ │ ├── seafile-auth-service.ts # Integrasi Seafile
|
||||
│ │ └── session.ts # iron-session helper
|
||||
│ │
|
||||
│ ├── middlewares/ # Next.js middleware
|
||||
│ ├── state/ # Global state (Jotai/Valtio)
|
||||
│ │ ├── darkModeStore.ts # State dark mode
|
||||
│ │ ├── state-layanan.ts # State layanan
|
||||
│ │ ├── state-list-image.ts # State daftar gambar
|
||||
│ │ └── state-nav.ts # State navigasi
|
||||
│ │
|
||||
│ ├── store/ # State management tambahan
|
||||
│ └── types/ # TypeScript type definitions
|
||||
│
|
||||
├── public/ # Static assets
|
||||
│ └── assets/ # Gambar, icon, dll
|
||||
│
|
||||
├── uploads/ # Directory upload (runtime)
|
||||
│ └── image/ # Upload gambar
|
||||
│
|
||||
├── .env.example # Contoh environment variables
|
||||
├── .gitignore
|
||||
├── AGENTS.md # Panduan untuk AI coding agents
|
||||
├── Dockerfile # Docker image definition
|
||||
├── docker-entrypoint.sh # Entry point container
|
||||
├── next.config.ts # Next.js configuration
|
||||
├── package.json # Dependencies & scripts
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── biome.json # Biome linter config
|
||||
├── eslint.config.mjs # ESLint config
|
||||
├── NOTE.md # Catatan deployment
|
||||
└── QWEN.md # Konteks & memori proyek
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Arsitektur Aplikasi
|
||||
|
||||
### 3.1 Arsitektur Keseluruhan
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Client (Browser) │
|
||||
└────────────┬────────────────────────────┬────────────────┘
|
||||
│ │
|
||||
│ Next.js Pages │ API Calls
|
||||
│ (SSR/CSR) │
|
||||
▼ ▼
|
||||
┌────────────────────────┐ ┌────────────────────────────┐
|
||||
│ Next.js 15 App Router│ │ Elysia.js API Server │
|
||||
│ - Pages publik │ │ - RESTful endpoints │
|
||||
│ - Admin dashboard │ │ - File upload │
|
||||
│ - Server components │ │ - Swagger docs (/api/docs│
|
||||
│ - Client components │ │ - Static file serving │
|
||||
└────────────┬───────────┘ └────────────┬───────────────┘
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ PostgreSQL Database │
|
||||
│ (via Prisma ORM) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Seafile File Storage │
|
||||
│ (Images & Documents) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 Next.js App Router
|
||||
|
||||
- Menggunakan **App Router** (bukan Pages Router)
|
||||
- Route groups `(dashboard)`, `(pages)`, `(tambahan)` untuk organisasi tanpa mempengaruhi URL
|
||||
- Layout bersarang: root layout -> admin/darmasaba layout -> page layouts
|
||||
- `force-dynamic` digunakan untuk menghindari error prerendering
|
||||
- View Transitions API diaktifkan via `next-view-transitions`
|
||||
|
||||
### 3.3 Elysia.js API Server
|
||||
|
||||
- Terintegrasi sebagai **catch-all route** di `/api/[[...slugs]]/route.ts`
|
||||
- Semua HTTP methods (GET, POST, PATCH, DELETE, PUT) di-handle oleh Elysia
|
||||
- Plugin yang digunakan:
|
||||
- `@elysiajs/cors` - CORS configuration
|
||||
- `@elysiajs/static` - Static file serving dari `/uploads`
|
||||
- `@elysiajs/swagger` - API documentation di `/api/docs`
|
||||
- `@elysiajs/jwt` - JWT authentication
|
||||
- `@elysiajs/cookie` - Cookie handling
|
||||
- Endpoint file upload: `/api/upl-img`, `/api/upl-img-single`, `/api/upl-csv`
|
||||
- Image serving: `/api/img/:name` dengan resize support
|
||||
|
||||
### 3.4 Rendering Strategy
|
||||
|
||||
- **Server Components**: Halaman publik untuk SEO optimal
|
||||
- **Client Components**: Komponen interaktif (form, state, animasi)
|
||||
- **Force Dynamic**: Beberapa halaman menggunakan `force-dynamic`
|
||||
- **ISR**: Caching header untuk assets (1 jam cache)
|
||||
|
||||
---
|
||||
|
||||
## 4. Modul Domain
|
||||
|
||||
### 4.1 Profil Desa (Desa)
|
||||
**Admin**: `/admin/desa/*` | **Publik**: `/darmasaba/desa/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `berita` | CRUD berita/pengumuman desa |
|
||||
| `gallery` | Galeri foto dan video |
|
||||
| `layanan` | Manajemen layanan desa |
|
||||
| `penghargaan` | Penghargaan yang diraih |
|
||||
| `pengumuman` | Pengumuman publik |
|
||||
| `potensi` | Potensi desa (pertanian, pariwisata, dll) |
|
||||
| `profil` | Profil desa (sejarah, visi misi, lambang, maskot, perangkat) |
|
||||
|
||||
### 4.2 PPID (Pejabat Pengelola Informasi dan Dokumentasi)
|
||||
**Admin**: `/admin/ppid/*` | **Publik**: `/darmasaba/ppid/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `profil-ppid` | Profil pejabat PPID |
|
||||
| `struktur-ppid` | Struktur organisasi PPID |
|
||||
| `visi-misi-ppid` | Visi dan misi PPID |
|
||||
| `daftar-informasi-publik` | Daftar informasi yang tersedia |
|
||||
| `dasar-hukum` | Dasar hukum PPID |
|
||||
| `permohonan-informasi-publik` | Form permohonan informasi |
|
||||
| `permohonan-keberatan-informasi-publik` | Form keberatan |
|
||||
| `indeks-kepuasan-masyarakat` | Survei kepuasan masyarakat (IKM) |
|
||||
|
||||
### 4.3 Kesehatan
|
||||
**Admin**: `/admin/kesehatan/*` | **Publik**: `/darmasaba/kesehatan/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `fasilitas-kesehatan` | Data puskesmas, klinik, dokter |
|
||||
| `posyandu` | Manajemen posyandu |
|
||||
| `program-kesehatan` | Program kesehatan desa |
|
||||
| `info-wabah-penyakit` | Informasi wabah |
|
||||
| `penanganan-darurat` | Prosedur penanganan darurat |
|
||||
| `kontak-darurat` | Kontak darurat kesehatan |
|
||||
| `data-kesehatan-warga` | Statistik kesehatan warga |
|
||||
| `artikel-kesehatan` | Artikel kesehatan |
|
||||
|
||||
### 4.4 Ekonomi
|
||||
**Admin**: `/admin/ekonomi/*` | **Publik**: `/darmasaba/ekonomi/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `APBDes` | Anggaran Pendapatan dan Belanja Desa (hierarki items + realisasi) |
|
||||
| `PADesa-pendapatan-asli-desa` | Pendapatan asli desa |
|
||||
| `demografi-pekerjaan` | Demografi pekerjaan penduduk |
|
||||
| `jumlah-penduduk-miskin` | Data penduduk miskin |
|
||||
| `jumlah-pengangguran` | Data pengangguran |
|
||||
| `lowongan-kerja-lokal` | Lowongan kerja lokal |
|
||||
| `pasar-desa` | Data pasar desa |
|
||||
| `program-kemiskinan` | Program penanganan kemiskinan |
|
||||
| `sektor-unggulan-desa` | Sektor unggulan ekonomi |
|
||||
| `Struktur-Organisasi-Dan-Sk-Pengurus-BumDes` | Struktur BUMDes |
|
||||
|
||||
### 4.5 Kependudukan
|
||||
**Admin**: `/admin/kependudukan/*` | **Publik**: `/darmasaba/kependudukan/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `data-banjar` | Data banjar (unit wilayah tradisional Bali) |
|
||||
| `distribusi-agama` | Distribusi agama penduduk |
|
||||
| `distribusi-umur` | Distribusi umur penduduk |
|
||||
| `migrasi-penduduk` | Data migrasi (masuk/keluar) |
|
||||
|
||||
### 4.6 Pendidikan
|
||||
**Admin**: `/admin/pendidikan/*` | **Publik**: `/darmasaba/pendidikan/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `beasiswa-desa` | Program beasiswa |
|
||||
| `bimbingan-belajar-desa` | Bimbingan belajar |
|
||||
| `data-pendidikan` | Data statistik pendidikan |
|
||||
| `info-sekolah` | Informasi sekolah |
|
||||
| `pendidikan-non-formal` | Pendidikan non-formal |
|
||||
| `perpustakaan-digital` | Perpustakaan digital |
|
||||
| `program-pendidikan-anak` | Program pendidikan anak |
|
||||
|
||||
### 4.7 Keamanan
|
||||
**Admin**: `/admin/keamanan/*` | **Publik**: `/darmasaba/keamanan/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `keamanan-lingkungan-pecalang-patwal` | Keamanan lingkungan (pecalang Bali) |
|
||||
| `kontak-darurat` | Kontak darurat keamanan |
|
||||
| `laporan-publik` | Laporan publik |
|
||||
| `pencegahan-kriminalitas` | Pencegahan kriminalitas |
|
||||
| `polsek-terdekat` | Data polsek terdekat |
|
||||
| `tips-keamanan` | Tips keamanan |
|
||||
|
||||
### 4.8 Lingkungan
|
||||
**Admin**: `/admin/lingkungan/*` | **Publik**: `/darmasaba/lingkungan/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `data-lingkungan-desa` | Data lingkungan desa |
|
||||
| `edukasi-lingkungan` | Edukasi lingkungan |
|
||||
| `gotong-royong` | Kegiatan gotong royong |
|
||||
| `konservasi-adat-bali` | Konservasi adat Bali |
|
||||
| `pengelolaan-sampah-bank-sampah` | Bank sampah |
|
||||
| `program-penghijauan` | Program penghijauan |
|
||||
|
||||
### 4.9 Inovasi
|
||||
**Admin**: `/admin/inovasi/*` | **Publik**: `/darmasaba/inovasi/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `ajukan-ide-inovatif` | Form pengajuan ide inovatif |
|
||||
| `desa-digital-smart-village` | Program desa digital |
|
||||
| `info-teknologi-tepat-guna` | Info teknologi tepat guna |
|
||||
| `kolaborasi-inovasi` | Kolaborasi inovasi |
|
||||
| `layanan-online-desa` | Layanan online desa |
|
||||
| `program-kreatif-desa` | Program kreatif desa |
|
||||
|
||||
### 4.10 Musik Desa
|
||||
**Admin**: `/admin/musik/*` | **Publik**: `/darmasaba/musik/*`
|
||||
|
||||
- Manajemen audio dan cover musik desa
|
||||
- Fixed player bar di halaman publik
|
||||
- Context provider untuk state pemutar musik
|
||||
|
||||
### 4.11 Landing Page
|
||||
**Admin**: `/admin/landing-page/*`
|
||||
|
||||
| Sub-modul | Fungsi |
|
||||
|-----------|--------|
|
||||
| `desa-anti-korupsi` | Konten anti-korupsi |
|
||||
| `prestasi-desa` | Prestasi yang diraih |
|
||||
| `sdgs-desa` | SDGs (Sustainable Development Goals) |
|
||||
| `profil-landing-page` | Profil dan media sosial |
|
||||
|
||||
### 4.12 User & Role
|
||||
**Admin**: `/admin/user&role/*`
|
||||
|
||||
- Manajemen pengguna admin
|
||||
- Manajemen role dan permission
|
||||
- Manajemen menu akses
|
||||
|
||||
---
|
||||
|
||||
## 5. Database Schema
|
||||
|
||||
### 5.1 Overview
|
||||
|
||||
Database menggunakan **PostgreSQL** dengan **Prisma ORM** (versi 6.3.1).
|
||||
Schema terdiri dari **2413 baris** dengan **100+ model**.
|
||||
|
||||
### 5.2 Model Utama
|
||||
|
||||
#### FileStorage
|
||||
Model sentral untuk semua file (gambar, dokumen, audio):
|
||||
```prisma
|
||||
model FileStorage {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
realName String
|
||||
path String
|
||||
mimeType String
|
||||
category String // "image" / "document" / "audio" / "other"
|
||||
link String
|
||||
isActive Boolean @default(true)
|
||||
// Relasi ke 50+ model lain (Berita, PotensiDesa, GalleryFoto, dll)
|
||||
}
|
||||
```
|
||||
|
||||
#### AppMenu & AppMenuChild
|
||||
Menu navigasi aplikasi:
|
||||
```prisma
|
||||
model AppMenu {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
link String
|
||||
isActive Boolean @default(true)
|
||||
AppMenuChild AppMenuChild[]
|
||||
}
|
||||
```
|
||||
|
||||
#### User & Role (Autentikasi Admin)
|
||||
- `User` - Data pengguna admin
|
||||
- `Role` - Role/peran pengguna
|
||||
- `Menu` - Menu akses per role
|
||||
|
||||
#### Modul Desa
|
||||
- `Berita` - Berita desa (dengan featured image & gallery)
|
||||
- `GalleryFoto` / `GalleryVideo` - Galeri media
|
||||
- `Layanan` - Layanan desa
|
||||
- `Pengumuman` - Pengumuman
|
||||
- `PotensiDesa` - Potensi desa
|
||||
- `ProfileDesaImage` - Gambar profil desa
|
||||
- `ProfilPerbekel` - Profil perbekel (kepala desa)
|
||||
- `PejabatDesa` - Pejabat desa
|
||||
- `Penghargaan` - Penghargaan
|
||||
- `PrestasiDesa` - Prestasi
|
||||
- `MediaSosial` - Media sosial desa
|
||||
|
||||
#### Modul PPID
|
||||
- `StrukturPPID` - Struktur organisasi
|
||||
- `PosisiOrganisasiPPID` - Posisi dengan hierarki
|
||||
- `PegawaiPPID` - Data pegawai
|
||||
- `ProfilePPID` - Profil PPID
|
||||
- `VisiMisiPPID` - Visi misi
|
||||
- `DasarHukumPPID` - Dasar hukum
|
||||
- `DaftarInformasiPublik` - Daftar informasi
|
||||
- `PermohonanInformasiPublik` - Permohonan informasi
|
||||
- `FormulirPermohonanKeberatan` - Formulir keberatan
|
||||
- `IndeksKepuasanMasyarakat` - IKM
|
||||
- `Responden` + lookup tables - Data responden IKM
|
||||
|
||||
#### Modul Kesehatan
|
||||
- `Puskesmas` - Data puskesmas
|
||||
- `Posyandu` - Data posyandu
|
||||
- `ProgramKesehatan` - Program kesehatan
|
||||
- `FasilitasKesehatan` - Fasilitas
|
||||
- `InfoWabahPenyakit` - Info wabah
|
||||
- `PenangananDarurat` - Penanganan darurat
|
||||
- `KontakDarurat` - Kontak darurat
|
||||
- `ArtikelKesehatan` - Artikel
|
||||
|
||||
#### Modul Ekonomi
|
||||
- `APBDes` & `APBDesItem` - Anggaran desa (hierarki tree structure)
|
||||
- `RealisasiItem` - Realisasi anggaran (multiple per item)
|
||||
- `PasarDesa` - Pasar desa
|
||||
- `PegawaiBumDes` - Pegawai BUMDes
|
||||
- `StrukturBumDes` - Struktur BUMDes
|
||||
- `DemografiPekerjaan` - Demografi pekerjaan
|
||||
- `JumlahPendudukMiskin` - Data kemiskinan
|
||||
- `JumlahPengangguran` - Data pengangguran
|
||||
- `LowonganKerjaLokal` - Lowongan kerja
|
||||
- `ProgramKemiskinan` - Program kemiskinan
|
||||
- `SektorUnggulanDesa` - Sektor unggulan
|
||||
- `PendapatanAsli` - Pendapatan asli desa
|
||||
|
||||
#### Modul Kependudukan
|
||||
- `DataBanjar` - Data banjar
|
||||
- `DistribusiAgama` - Distribusi agama
|
||||
- `DistribusiUmur` - Distribusi umur
|
||||
- `MigrasiPenduduk` - Migrasi
|
||||
|
||||
#### Modul Pendidikan
|
||||
- `InfoSekolah` - Data sekolah
|
||||
- `BeasiswaDesa` - Beasiswa
|
||||
- `BimbinganBelajar` - Bimbingan belajar
|
||||
- `PendidikanNonFormal` - Pendidikan non-formal
|
||||
- `DataPerpustakaan` - Perpustakaan
|
||||
|
||||
#### Modul Keamanan
|
||||
- `KeamananLingkungan` - Keamanan lingkungan
|
||||
- `MenuTipsKeamanan` - Tips keamanan
|
||||
- `PencegahanKriminalitas` - Pencegahan kriminalitas
|
||||
- `PolsekTerdekat` - Polsek terdekat
|
||||
- `LaporanPublik` - Laporan publik
|
||||
|
||||
#### Modul Lingkungan
|
||||
- `DataLingkunganDesa` - Data lingkungan
|
||||
- `KonservasiAdatBali` - Konservasi adat
|
||||
- `BankSampah` - Bank sampah
|
||||
- `ProgramPenghijauan` - Penghijauan
|
||||
- `GotongRoyong` - Gotong royong
|
||||
- `EdukasiLingkungan` - Edukasi
|
||||
|
||||
#### Modul Inovasi
|
||||
- `ProgramInovasi` - Program inovasi
|
||||
- `DesaDigital` - Desa digital
|
||||
- `InfoTekno` - Info teknologi
|
||||
- `KolaborasiInovasi` + `MitraKolaborasi` - Kolaborasi
|
||||
- `LayananOnlineDesa` - Layanan online
|
||||
- `ProgramKreatifDesa` - Program kreatif
|
||||
- `Ajukan` - Pengajuan ide
|
||||
|
||||
#### Modul Musik
|
||||
- `MusikDesa` - Musik desa
|
||||
- `audioFile` -> FileStorage
|
||||
- `coverImage` -> FileStorage
|
||||
|
||||
#### Landing Page
|
||||
- `DesaAntiKorupsi` + `KategoriDesaAntiKorupsi`
|
||||
- `SdgsDesa` - SDGs
|
||||
- `PrestasiDesa` + `KategoriPrestasiDesa`
|
||||
- `MediaSosial`
|
||||
- `LandingPage_Layanan`
|
||||
|
||||
#### APBDes (Struktur Hierarki)
|
||||
```prisma
|
||||
model APBDesItem {
|
||||
kode String // "4", "4.1", "4.1.2"
|
||||
uraian String // Nama item
|
||||
anggaran Float // Anggaran dalam Rupiah
|
||||
tipe String? // "pendapatan" | "belanja" | "pembiayaan"
|
||||
level Int // 1, 2, 3
|
||||
parentId String? // Self-referencing untuk tree
|
||||
children APBDesItem[]
|
||||
totalRealisasi Float @default(0) // Auto-calculated
|
||||
selisih Float @default(0) // totalRealisasi - anggaran
|
||||
persentase Float @default(0) // (totalRealisasi / anggaran) * 100
|
||||
realisasiItems RealisasiItem[]
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Pola Umum Model
|
||||
|
||||
Hampir semua model mengikuti pola:
|
||||
```prisma
|
||||
model Contoh {
|
||||
id String @id @default(cuid())
|
||||
// ... fields
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime? @default(now()) // Soft delete
|
||||
isActive Boolean @default(true) // Soft delete flag
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. API Routes
|
||||
|
||||
### 6.1 Struktur API
|
||||
|
||||
Semua API routes ditangani oleh **Elysia.js** di `/src/app/api/[[...slugs]]/route.ts`
|
||||
|
||||
### 6.2 API Groups
|
||||
|
||||
| Prefix | Modul | Contoh Endpoints |
|
||||
|--------|-------|------------------|
|
||||
| `/api/layanan` | Layanan | `GET /api/layanan` |
|
||||
| `/api/potensi` | Potensi | `GET /api/potensi` |
|
||||
| `/api/desa/*` | Desa | CRUD berita, gallery, profil, dll |
|
||||
| `/api/ppid/*` | PPID | CRUD struktur, profil, permohonan |
|
||||
| `/api/kesehatan/*` | Kesehatan | CRUD puskesmas, posyandu, dll |
|
||||
| `/api/ekonomi/*` | Ekonomi | CRUD APBDes, BUMDes, demografi |
|
||||
| `/api/kependudukan/*` | Kependudukan | CRUD banjar, demografi |
|
||||
| `/api/pendidikan/*` | Pendidikan | CRUD sekolah, beasiswa |
|
||||
| `/api/keamanan/*` | Keamanan | CRUD keamanan, kontak darurat |
|
||||
| `/api/lingkungan/*` | Lingkungan | CRUD data lingkungan |
|
||||
| `/api/inovasi/*` | Inovasi | CRUD program inovasi |
|
||||
| `/api/landing-page/*` | Landing Page | CRUD konten landing page |
|
||||
| `/api/user/*` | User | CRUD user admin |
|
||||
| `/api/user/role/*` | Role | CRUD role & permission |
|
||||
| `/api/search` | Search | Pencarian global |
|
||||
| `/api/file-storage/*` | File Storage | CRUD file storage |
|
||||
| `/api/img/:name` | Image | GET gambar dengan resize |
|
||||
| `/api/upl-img` | Upload | Upload multiple images |
|
||||
| `/api/upl-img-single` | Upload | Upload single image |
|
||||
| `/api/upl-csv` | Upload | Upload CSV files |
|
||||
| `/api/utils/version` | Utils | GET versi aplikasi |
|
||||
|
||||
### 6.3 API Documentation
|
||||
|
||||
Swagger UI tersedia di: **`/api/docs`**
|
||||
|
||||
### 6.4 API Route Lainnya
|
||||
|
||||
| Route | Fungsi |
|
||||
|-------|--------|
|
||||
| `/api/health` | Health check endpoint |
|
||||
| `/api/news` | API berita (standalone) |
|
||||
| `/api/subscribe` | Subscription email |
|
||||
| `/api/tts` | Text-to-Speech (ElevenLabs) |
|
||||
| `/api/admin/*` | API khusus admin |
|
||||
| `/api/auth/*` | API autentikasi |
|
||||
|
||||
---
|
||||
|
||||
## 7. Halaman Admin
|
||||
|
||||
### 7.1 Struktur
|
||||
|
||||
Admin dashboard berada di `/admin` dengan route group `(dashboard)`.
|
||||
|
||||
| Section | Path | Fungsi |
|
||||
|---------|------|--------|
|
||||
| **Dashboard** | `/admin` | Dashboard utama |
|
||||
| **Autentikasi** | `/admin/auth` | Login admin |
|
||||
| **Desa** | `/admin/desa/*` | Berita, gallery, profil, layanan, penghargaan, pengumuman, potensi |
|
||||
| **PPID** | `/admin/ppid/*` | Profil, struktur, visi-misi, daftar informasi, dasar hukum, permohonan, IKM |
|
||||
| **Kesehatan** | `/admin/kesehatan/*` | Puskesmas, posyandu, program kesehatan, wabah, kontak darurat |
|
||||
| **Ekonomi** | `/admin/ekonomi/*` | APBDes, PAD, demografi, pengangguran, kemiskinan, BUMDes, pasar desa |
|
||||
| **Kependudukan** | `/admin/kependudukan/*` | Banjar, distribusi agama, distribusi umur, migrasi |
|
||||
| **Pendidikan** | `/admin/pendidikan/*` | Sekolah, beasiswa, bimbingan belajar, perpustakaan digital |
|
||||
| **Keamanan** | `/admin/keamanan/*` | Keamanan lingkungan, kontak darurat, pencegahan kriminalitas, polsek |
|
||||
| **Lingkungan** | `/admin/lingkungan/*` | Data lingkungan, konservasi, bank sampah, penghijauan, gotong royong |
|
||||
| **Inovasi** | `/admin/inovasi/*` | Ide inovatif, desa digital, teknologi tepat guna, kolaborasi |
|
||||
| **Musik** | `/admin/musik/*` | Manajemen musik desa |
|
||||
| **Landing Page** | `/admin/landing-page/*` | Anti-korupsi, prestasi, SDGs, media sosial |
|
||||
| **User & Role** | `/admin/user&role/*` | Manajemen user dan role |
|
||||
| **Images** | `/admin/images/*` | Manajemen gambar |
|
||||
| **CSV** | `/admin/csv/*` | Upload/import CSV |
|
||||
|
||||
### 7.2 Komponen Admin Shared
|
||||
|
||||
- `AdminThemeProvider.tsx` - Theme provider untuk dark/light mode
|
||||
- `DarkModeToggle.tsx` - Toggle dark mode
|
||||
- `UnifiedSurface.tsx` - Komponen surface/card unified
|
||||
- `UnifiedTypography.tsx` - Tipografi unified
|
||||
|
||||
---
|
||||
|
||||
## 8. Halaman Publik
|
||||
|
||||
### 8.1 Struktur
|
||||
|
||||
Halaman publik berada di `/darmasaba` dengan layout yang mencakup Navbar, Footer, dan Fixed Music Player.
|
||||
|
||||
| Halaman | Path | Konten |
|
||||
|---------|------|--------|
|
||||
| **Landing Page
|
||||
842
STRUKTUR.md
Normal file
842
STRUKTUR.md
Normal file
@@ -0,0 +1,842 @@
|
||||
# Dokumentasi Struktur Proyek Desa Darmasaba
|
||||
|
||||
## 1. Ringkasan Proyek
|
||||
|
||||
**Desa Darmasaba** adalah aplikasi web manajemen desa digital untuk Desa Darmasaba, Kabupaten Badung, Bali. Aplikasi ini berfungsi sebagai platform layanan publik digital yang mencakup informasi pemerintahan, layanan kesehatan, keamanan, pendidikan, ekonomi, lingkungan, dan inovasi desa.
|
||||
|
||||
### Tech Stack
|
||||
|
||||
| Kategori | Teknologi |
|
||||
|----------|-----------|
|
||||
| **Framework** | Next.js 15 (App Router) |
|
||||
| **Language** | TypeScript (strict mode) |
|
||||
| **Runtime** | Bun |
|
||||
| **Backend API** | Elysia.js (high-performance HTTP server) |
|
||||
| **Database** | PostgreSQL |
|
||||
| **ORM** | Prisma 6.3.1 |
|
||||
| **UI Framework** | Mantine UI v7-v8 |
|
||||
| **State Management** | Jotai + Valtio + SWR |
|
||||
| **Authentication** | iron-session + JWT (@elysiajs/jwt) |
|
||||
| **File Storage** | Seafile (self-hosted) |
|
||||
| **Text Editor** | Tiptap (Rich text editor) |
|
||||
| **Charts** | Recharts + Chart.js |
|
||||
| **Maps** | Leaflet + react-leaflet |
|
||||
| **Testing** | Vitest (unit) + Playwright (E2E) |
|
||||
| **Styling** | Mantine + PostCSS + Framer Motion |
|
||||
| **Deployment** | Docker + GHCR + Portainer + GitHub Actions |
|
||||
| **Version** | 0.1.11 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Struktur Direktori
|
||||
|
||||
```
|
||||
desa-darmasaba/
|
||||
├── .github/workflows/ # GitHub Actions CI/CD
|
||||
│ ├── docker-publish.yml # Auto build & push saat tag v*
|
||||
│ ├── publish.yml # Manual build & push ke GHCR
|
||||
│ ├── re-pull.yml # Manual re-pull di Portainer
|
||||
│ └── script/ # Shell scripts untuk deploy
|
||||
├── prisma/
|
||||
│ ├── schema.prisma # Database schema (2413 baris, 100+ model)
|
||||
│ └── seed.ts # Database seeder (400+ baris)
|
||||
│ └── _seeder_list/ # Seed data per modul
|
||||
├── public/ # Static assets
|
||||
│ └── assets/
|
||||
│ └── images/
|
||||
├── src/
|
||||
│ ├── app/ # Next.js App Router
|
||||
│ │ ├── _com/ # Global components (SplashScreen, WebVitals)
|
||||
│ │ ├── admin/ # ADMIN DASHBOARD
|
||||
│ │ │ ├── (dashboard)/ # Route group dashboard
|
||||
│ │ │ │ ├── desa/ # - Berita, Gallery, Layanan, dll
|
||||
│ │ │ │ ├── ppid/ # - Informasi publik, struktur, dasar hukum
|
||||
│ │ │ │ ├── kesehatan/ # - Fasilitas, posyandu, puskesmas, wabah
|
||||
│ │ │ │ ├── ekonomi/ # - APBDes, pasar desa, BUMDes, dll
|
||||
│ │ │ │ ├── kependudukan/ # - Banjar, agama, umur, migrasi
|
||||
│ │ │ │ ├── pendidikan/ # - Sekolah, beasiswa, perpustakaan
|
||||
│ │ │ │ ├── keamanan/ # - Keamanan lingkungan, polsek, dll
|
||||
│ │ │ │ ├── lingkungan/ # - Sampah, penghijauan, gotong royong
|
||||
│ │ │ │ ├── inovasi/ # - Desa digital, kolaborasi, dll
|
||||
│ │ │ │ ├── landing-page/ # - Profil, prestasi, anti-korupsi
|
||||
│ │ │ │ ├── musik/ # - Musik desa
|
||||
│ │ │ │ ├── user&role/ # - Manajemen user & role
|
||||
│ │ │ │ └── _com/ # - Shared admin components
|
||||
│ │ │ ├── auth/ # Login OTP untuk admin
|
||||
│ │ │ ├── csv/ # Demo CSV upload
|
||||
│ │ │ └── layout.tsx # Admin shell (AppShell Mantine)
|
||||
│ │ ├── api/ # ELYSIA.JS API SERVER
|
||||
│ │ │ ├── [[...slugs]]/ # Catch-all route -> Elysia handler
|
||||
│ │ │ │ ├── route.ts # - Main Elysia server export
|
||||
│ │ │ │ └── _lib/ # - Domain route modules
|
||||
│ │ │ │ ├── desa.ts
|
||||
│ │ │ │ ├── ppid.ts
|
||||
│ │ │ │ ├── kesehatan.ts
|
||||
│ │ │ │ ├── ekonomi.ts
|
||||
│ │ │ │ ├── keamanan.ts
|
||||
│ │ │ │ ├── inovasi.ts
|
||||
│ │ │ │ ├── lingkungan.ts
|
||||
│ │ │ │ ├── pendidikan.ts
|
||||
│ │ │ │ ├── kependudukan.ts
|
||||
│ │ │ │ ├── landing_page.ts
|
||||
│ │ │ │ ├── user/ # - User & Role management
|
||||
│ │ │ │ ├── fileStorage/
|
||||
│ │ │ │ ├── search/
|
||||
│ │ │ │ ├── auth/
|
||||
│ │ │ │ ├── upl-img.ts, upl-img-single.ts
|
||||
│ │ │ │ ├── upl-csv.ts, upl-csv-single.ts
|
||||
│ │ │ │ └── img.ts, img-del.ts, imgs.ts
|
||||
│ │ │ ├── auth/ # Auth endpoints (login, logout, me)
|
||||
│ │ │ └── ... # Other API routes
|
||||
│ │ ├── darmasaba/ # PUBLIC-FACING WEBSITE
|
||||
│ │ │ ├── _com/ # Shared components (Navbar, Footer, etc)
|
||||
│ │ │ ├── (pages)/ # Public pages route group
|
||||
│ │ │ │ ├── desa/ # - Profil, berita, gallery, layanan
|
||||
│ │ │ │ ├── ppid/ # - PPID public pages
|
||||
│ │ │ │ ├── kesehatan/ # - Health info pages
|
||||
│ │ │ │ ├── ekonomi/ # - Economy pages
|
||||
│ │ │ │ ├── kependudukan/
|
||||
│ │ │ │ ├── pendidikan/
|
||||
│ │ │ │ ├── keamanan/
|
||||
│ │ │ │ ├── lingkungan/
|
||||
│ │ │ │ ├── inovasi/
|
||||
│ │ │ │ ├── musik/
|
||||
│ │ │ │ └── module/ # - External module links
|
||||
│ │ │ └── (tambahan)/ # Additional pages
|
||||
│ │ ├── login/ # Login page
|
||||
│ │ ├── registrasi/ # Registration page
|
||||
│ │ ├── waiting-room/ # Waiting room (inactive users)
|
||||
│ │ ├── terms-of-service/
|
||||
│ │ ├── layout.tsx # Root layout (MantineProvider, ViewTransitions)
|
||||
│ │ └── page.tsx # Homepage redirect
|
||||
│ ├── components/
|
||||
│ │ └── admin/ # Admin shared components
|
||||
│ │ ├── AdminThemeProvider.tsx
|
||||
│ │ ├── DarkModeToggle.tsx
|
||||
│ │ ├── UnifiedSurface.tsx
|
||||
│ │ └── UnifiedTypography.tsx
|
||||
│ ├── con/ # Constants & configuration
|
||||
│ │ ├── colors.ts # Color palette definitions
|
||||
│ │ ├── images.ts
|
||||
│ │ ├── navbar-list-menu.ts
|
||||
│ │ ├── router.ts # Route mapping
|
||||
│ │ └── sosmed.ts
|
||||
│ ├── context/ # React contexts
|
||||
│ │ └── MusicContext.tsx # Music player context
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ ├── lib/ # Utility libraries
|
||||
│ │ ├── router/
|
||||
│ │ ├── api-auth.ts # API authentication helpers
|
||||
│ │ ├── api-fetch.ts # API fetch wrapper
|
||||
│ │ ├── EnvStringParse.ts
|
||||
│ │ ├── prisma.ts # Prisma client singleton
|
||||
│ │ ├── seafile-auth-service.ts
|
||||
│ │ └── session.ts # iron-session helper
|
||||
│ ├── state/ # Global state (Jotai/Valtio)
|
||||
│ │ ├── darkModeStore.ts
|
||||
│ │ ├── state-layanan.ts
|
||||
│ │ ├── state-list-image.ts
|
||||
│ │ └── state-nav.ts
|
||||
│ ├── store/ # Additional stores
|
||||
│ │ └── authStore.ts # Auth state (Jotai)
|
||||
│ ├── types/ # TypeScript type definitions
|
||||
│ └── utils/ # Utility functions
|
||||
│ └── themeTokens.ts # Dark/light theme tokens
|
||||
├── uploads/ # Local upload directory (images/files)
|
||||
├── Dockerfile # Multi-stage Docker build (Bun)
|
||||
├── docker-entrypoint.sh # Entry script (migrate + start)
|
||||
├── next.config.ts # Next.js configuration
|
||||
├── package.json # Dependencies & scripts
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── biome.json # Biome linter config
|
||||
├── eslint.config.mjs # ESLint config
|
||||
├── NOTE.md # Deployment notes
|
||||
├── QWEN.md # Project memory & workflow
|
||||
└── AGENTS.md # Agent coding guidelines
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Arsitektur
|
||||
|
||||
### Pola Arsitektur: Full-Stack Monolith dengan App Router
|
||||
|
||||
```
|
||||
Browser
|
||||
|
|
||||
+-- Next.js 15 (App Router) -- Server Components + Client Components
|
||||
|
|
||||
+-- /darmasaba/* -> Public pages (SSR/CSR)
|
||||
+-- /admin/* -> Admin dashboard (protected)
|
||||
+-- /api/* -> Elysia.js API server
|
||||
|
|
||||
+-- Elysia Server (src/app/api/[[...slugs]]/route.ts)
|
||||
|
|
||||
+-- CORS enabled
|
||||
+-- Swagger docs di /api/docs
|
||||
+-- Static file serving (/api/uploads)
|
||||
+-- Domain modules: Desa, PPID, Kesehatan, Ekonomi, dll
|
||||
+-- Image upload handlers
|
||||
|
|
||||
+-- Prisma ORM --> PostgreSQL
|
||||
+-- Seafile API --> File Storage
|
||||
```
|
||||
|
||||
### Key Architectural Decisions:
|
||||
|
||||
1. **Next.js 15 App Router**: Menggunakan React Server Components sebagai default, dengan `"use client"` untuk interaktivitas
|
||||
2. **Elysia.js di dalam API Routes**: Catch-all route `[[...slugs]]` meneruskan semua request ke Elysia handler
|
||||
3. **Route Groups**: `(dashboard)` dan `(pages)` untuk organisasi tanpa mempengaruhi URL path
|
||||
4. **Multi-tenant Ready**: Role-based access control dengan dynamic navbar berdasarkan roleId
|
||||
5. **File Uploads**: Local uploads + Seafile integration untuk distributed storage
|
||||
|
||||
---
|
||||
|
||||
## 4. Modul Domain
|
||||
|
||||
### A. PPID (Pejabat Pengelola Informasi dan Dokumentasi)
|
||||
**Lokasi**: `src/app/admin/(dashboard)/ppid/` dan `src/app/darmasaba/(pages)/ppid/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Profil PPID | Profil pejabat pengelola informasi |
|
||||
| Struktur PPID | Struktur organisasi PPID dengan hierarki |
|
||||
| Visi & Misi PPID | Visi dan misi PPID desa |
|
||||
| Daftar Informasi Publik | Katalog informasi publik yang tersedia |
|
||||
| Dasar Hukum | Regulasi dan dasar hukum PPID |
|
||||
| Permohonan Informasi Publik | Form permohonan informasi (NIK, kontak, jenis) |
|
||||
| Permohonan Keberatan | Formulir keberatan informasi |
|
||||
| Indeks Kepuasan Masyarakat | Survey kepuasan dengan grafik demografis |
|
||||
|
||||
### B. Desa (Landing Page & Umum)
|
||||
**Lokasi**: `src/app/admin/(dashboard)/desa/` dan `src/app/darmasaba/(pages)/desa/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Profil Desa | Sejarah, visi-misi, lambang, maskot |
|
||||
| Profil Perbekel | Biodata, pengalaman, program unggulan perbekel |
|
||||
| Perbekel dari Masa ke Masa | Historis perbekel per periode |
|
||||
| Berita | Artikel berita dengan kategori & multi-image |
|
||||
| Gallery | Foto dan video galeri |
|
||||
| Pengumuman | Pengumuman desa dengan kategori |
|
||||
| Potensi Desa | Potensi desa dengan kategori |
|
||||
| Layanan Desa | Surat keterangan, ajukan permohonan |
|
||||
| Penghargaan | Prestasi dan penghargaan desa |
|
||||
| Desa Anti Korupsi | Transparansi anti-korupsi |
|
||||
| SDGs Desa | Sustainable Development Goals desa |
|
||||
| APBDes | Anggaran desa dengan hierarki item & realisasi |
|
||||
| Prestasi Desa | Katalog prestasi |
|
||||
|
||||
### C. Kesehatan
|
||||
**Lokasi**: `src/app/admin/(dashboard)/kesehatan/` dan `src/app/darmasaba/(pages)/kesehatan/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Fasilitas Kesehatan | Info rumah sakit/klinik (jam, dokter, tarif) |
|
||||
| Puskesmas | Data puskesmas dengan jam operasional & kontak |
|
||||
| Posyandu | Jadwal dan informasi posyandu |
|
||||
| Program Kesehatan | Program-program kesehatan desa |
|
||||
| Penanganan Darurat | Prosedur penanganan darurat |
|
||||
| Kontak Darurat | Kontak emergency dengan WhatsApp |
|
||||
| Info Wabah Penyakit | Informasi wabah penyakit |
|
||||
| Artikel Kesehatan | Artikel kesehatan lengkap |
|
||||
| Data Kesehatan Warga | Statistik kesehatan warga |
|
||||
| Kelahiran & Kematian | Data vital statistik |
|
||||
| Grafik Kepuasan | Grafik kepuasan layanan kesehatan |
|
||||
|
||||
### D. Ekonomi
|
||||
**Lokasi**: `src/app/admin/(dashboard)/ekonomi/` dan `src/app/darmasaba/(pages)/ekonomi/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Pasar Desa | Katalog pasar desa dengan produk & rating |
|
||||
| Struktur BUMDes | Organisasi BUMDes dengan pengurus |
|
||||
| APBDes (PADesa) | Pendapatan Asli Desa |
|
||||
| Program Kemiskinan | Program dan statistik kemiskinan |
|
||||
| Sektor Unggulan | Sektor ekonomi unggulan desa |
|
||||
| Lowongan Kerja Lokal | Info lowongan pekerjaan |
|
||||
| Demografi Pekerjaan | Distribusi pekerjaan penduduk |
|
||||
| Jumlah Pengangguran | Statistik pengangguran |
|
||||
| Penduduk Usia Kerja Menganggur | Analisis pengangguran by usia & pendidikan |
|
||||
| Jumlah Penduduk Miskin | Tren kemiskinan tahunan |
|
||||
|
||||
### E. Kependudukan
|
||||
**Lokasi**: `src/app/admin/(dashboard)/kependudukan/` dan `src/app/darmasaba/(pages)/kependudukan/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Data Banjar | Data penduduk per banjar |
|
||||
| Distribusi Agama | Statistik agama penduduk |
|
||||
| Distribusi Umur | Piramida umur penduduk |
|
||||
| Migrasi Penduduk | Data migrasi masuk/keluar |
|
||||
| Dinamika Penduduk | Kelahiran, kematian, migrasi per tahun |
|
||||
|
||||
### F. Pendidikan
|
||||
**Lokasi**: `src/app/admin/(dashboard)/pendidikan/` dan `src/app/darmasaba/(pages)/pendidikan/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Info Sekolah & PAUD | Data sekolah per jenjang (TK, SD, SMP, SMA) |
|
||||
| Beasiswa Desa | Program beasiswa & pendaftar |
|
||||
| Program Pendidikan Anak | Program pendidikan anak |
|
||||
| Bimbingan Belajar | Informasi bimbingan belajar |
|
||||
| Pendidikan Non Formal | Tempat & program non-formal |
|
||||
| Perpustakaan Digital | Katalog buku & peminjaman |
|
||||
| Data Pendidikan | Statistik pendidikan |
|
||||
|
||||
### G. Keamanan
|
||||
**Lokasi**: `src/app/admin/(dashboard)/keamanan/` dan `src/app/darmasaba/(pages)/keamanan/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Keamanan Lingkungan (Pecalang/Patwal) | Sistem keamanan tradisional Bali |
|
||||
| Polsek Terdekat | Data polsek dengan layanan & map |
|
||||
| Kontak Darurat | Kontak darurat keamanan |
|
||||
| Pencegahan Kriminalitas | Info pencegahan kriminal |
|
||||
| Laporan Publik | Laporan masyarakat dengan tracking status |
|
||||
| Tips Keamanan | Tips dan panduan keamanan |
|
||||
|
||||
### H. Lingkungan
|
||||
**Lokasi**: `src/app/admin/(dashboard)/lingkungan/` dan `src/app/darmasaba/(pages)/lingkungan/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Pengelolaan Sampah | Bank sampah & pengelolaan |
|
||||
| Program Penghijauan | Program penghijauan desa |
|
||||
| Data Lingkungan | Data lingkungan desa |
|
||||
| Gotong Royong | Kegiatan gotong royong |
|
||||
| Edukasi Lingkungan | Edukasi lingkungan hidup |
|
||||
| Konservasi Adat Bali | Tri Hita Karana & konservasi adat |
|
||||
|
||||
### I. Inovasi
|
||||
**Lokasi**: `src/app/admin/(dashboard)/inovasi/` dan `src/app/darmasaba/(pages)/inovasi/`
|
||||
|
||||
| Sub-modul | Deskripsi |
|
||||
|-----------|-----------|
|
||||
| Desa Digital (Smart Village) | Transformasi digital desa |
|
||||
| Program Kreatif Desa | Program kreatif & inovatif |
|
||||
| Kolaborasi Inovasi | Kolaborasi dengan mitra |
|
||||
| Info Teknologi Tepat Guna | Info teknologi untuk desa |
|
||||
| Ajukan Ide Inovatif | Form pengajuan ide dari warga |
|
||||
| Layanan Online Desa | Layanan administrasi online |
|
||||
|
||||
### J. Musik Desa
|
||||
**Lokasi**: `src/app/admin/(dashboard)/musik/` dan `src/app/darmasaba/(pages)/musik/`
|
||||
|
||||
Model `MusikDesa` dengan audio file, cover image, genre, dan durasi. Dilengkapi dengan `FixedPlayerBar` di layout publik.
|
||||
|
||||
### K. User & Role (Admin)
|
||||
**Lokasi**: `src/app/admin/(dashboard)/user&role/`
|
||||
|
||||
- **Role-based Access Control**: Role dengan permission JSON
|
||||
- **User Session Management**: Multiple sessions per user dengan JWT
|
||||
- **OTP Authentication**: Login dengan nomor telepon + OTP
|
||||
- **Menu Access Control**: Dynamic navbar berdasarkan menu akses user
|
||||
|
||||
---
|
||||
|
||||
## 5. Database Schema (Prisma)
|
||||
|
||||
Schema terdiri dari **2413 baris** dengan **100+ model** dan **berbagai enum**. Berikut model-model utama:
|
||||
|
||||
### Core Models
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `FileStorage` | Central file storage untuk semua uploaded files |
|
||||
| `AppMenu` / `AppMenuChild` | Menu navigasi aplikasi |
|
||||
| `User` / `Role` / `UserSession` / `UserMenuAccess` | Sistem autentikasi & otorisasi |
|
||||
| `KodeOtp` | OTP codes untuk login |
|
||||
|
||||
### Landing Page & Desa
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `PejabatDesa` | Pejabat desa dengan foto |
|
||||
| `ProfilPerbekel` | Profil perbekel (biodata, pengalaman, program) |
|
||||
| `PerbekelDariMasaKeMasa` | Historis perbekel |
|
||||
| `Berita` / `KategoriBerita` | Berita desa |
|
||||
| `PotensiDesa` / `KategoriPotensi` | Potensi desa |
|
||||
| `Pengumuman` / `CategoryPengumuman` | Pengumuman |
|
||||
| `GalleryFoto` / `GalleryVideo` | Gallery media |
|
||||
| `Penghargaan` | Penghargaan desa |
|
||||
| `APBDes` / `APBDesItem` / `RealisasiItem` | Anggaran dengan realisasi |
|
||||
| `DesaAntiKorupsi` / `KategoriDesaAntiKorupsi` | Transparansi |
|
||||
| `SdgsDesa` | SDGs desa |
|
||||
| `PrestasiDesa` / `KategoriPrestasiDesa` | Prestasi |
|
||||
| `MusikDesa` | Musik desa |
|
||||
|
||||
### PPID
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `StrukturPPID` / `PosisiOrganisasiPPID` / `PegawaiPPID` | Struktur organisasi |
|
||||
| `VisiMisiPPID` | Visi misi |
|
||||
| `ProfilePPID` | Profil pejabat |
|
||||
| `DasarHukumPPID` | Regulasi |
|
||||
| `DaftarInformasiPublik` | Katalog informasi |
|
||||
| `PermohonanInformasiPublik` | Permohonan + lookup tables |
|
||||
| `FormulirPermohonanKeberatan` | Keberatan |
|
||||
| `IndeksKepuasanMasyarakat` + grafik breakdown | Survey kepuasan |
|
||||
|
||||
### Kesehatan
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `FasilitasKesehatan` | Fasilitas lengkap (dokter, tarif, prosedur) |
|
||||
| `Puskesmas` / `JamOperasional` / `KontakPuskesmas` | Puskesmas |
|
||||
| `Posyandu` | Pos pelayanan terpadu |
|
||||
| `ProgramKesehatan` | Program kesehatan |
|
||||
| `ArtikelKesehatan` | Artikel lengkap (gejala, pencegahan, P3K, dll) |
|
||||
| `PenangananDarurat` / `KontakDarurat` | Darurat |
|
||||
| `InfoWabahPenyakit` | Wabah |
|
||||
| `DataKematian_Kelahiran` / `Kelahiran` / `Kematian` | Vital statistik |
|
||||
| `GrafikKepuasan` | Kepuasan |
|
||||
|
||||
### Ekonomi
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `PasarDesa` / `KategoriProduk` / `KategoriToPasar` | Pasar desa |
|
||||
| `StrukturBumDes` / `PosisiOrganisasiBumDes` / `PegawaiBumDes` | BUMDes |
|
||||
| `ProgramKemiskinan` / `StatistikKemiskinan` | Kemiskinan |
|
||||
| `SektorUnggulanDesa` | Sektor unggulan |
|
||||
| `LowonganPekerjaan` | Lowongan |
|
||||
| `DataDemografiPekerjaan` | Demografi pekerjaan |
|
||||
| `DetailDataPengangguran` | Pengangguran |
|
||||
| `GrafikJumlahPendudukMiskin` | Tren kemiskinan |
|
||||
|
||||
### Kependudukan
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `DataBanjar` | Data per banjar |
|
||||
| `DistribusiAgama` | Distribusi agama |
|
||||
| `DistribusiUmur` | Distribusi umur |
|
||||
| `MigrasiPenduduk` | Migrasi (MASUK/KELUAR) |
|
||||
| `DinamikaPenduduk` | Dinamika tahunan |
|
||||
|
||||
### Pendidikan
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `JenjangPendidikan` / `Lembaga` / `Siswa` / `Pengajar` | Data sekolah |
|
||||
| `BeasiswaPendaftar` | Beasiswa (dengan enum lengkap) |
|
||||
| `DataPerpustakaan` / `KategoriBuku` / `PeminjamanBuku` | Perpustakaan |
|
||||
| `DataPendidikan` | Statistik |
|
||||
|
||||
### Keamanan
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `KeamananLingkungan` | Keamanan lingkungan |
|
||||
| `PolsekTerdekat` / `LayananPolsek` / `LayananToPolsek` | Polsek |
|
||||
| `KontakDaruratKeamanan` / `KontakItem` | Kontak darurat |
|
||||
| `PencegahanKriminalitas` | Pencegahan |
|
||||
| `LaporanPublik` / `PenangananLaporanPublik` (enum StatusLaporan) | Laporan |
|
||||
| `Pelapor` | Pelapor |
|
||||
| `MenuTipsKeamanan` | Tips |
|
||||
|
||||
### Lingkungan
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `PengelolaanSampah` | Pengelolaan sampah |
|
||||
| `KeteranganBankSampahTerdekat` | Bank sampah |
|
||||
| `ProgramPenghijauan` | Penghijauan |
|
||||
| `DataLingkunganDesa` | Data lingkungan |
|
||||
| `KegiatanDesa` / `KategoriKegiatan` | Gotong royong |
|
||||
| `FilosofiTriHita` / `BentukKonservasiBerdasarkanAdat` | Konservasi Bali |
|
||||
|
||||
### Inovasi
|
||||
|
||||
| Model | Keterangan |
|
||||
|-------|-----------|
|
||||
| `DesaDigital` | Smart village |
|
||||
| `ProgramKreatif` | Program kreatif |
|
||||
| `KolaborasiInovasi` / `MitraKolaborasi` | Kolaborasi |
|
||||
| `InfoTekno` | Teknologi tepat guna |
|
||||
| `AjukanIdeInovatif` | Ide dari warga |
|
||||
| `AdministrasiOnline` / `JenisLayanan` | Layanan online |
|
||||
| `PengaduanMasyarakat` / `JenisPengaduan` | Pengaduan |
|
||||
|
||||
---
|
||||
|
||||
## 6. API Routes
|
||||
|
||||
Semua API ditangani oleh **Elysia.js** di `src/app/api/[[...slugs]]/route.ts`:
|
||||
|
||||
| Endpoint Group | Prefix | Deskripsi |
|
||||
|---------------|--------|-----------|
|
||||
| **File Storage** | `/api/file-storage` | CRUD file storage |
|
||||
| **Landing Page** | `/api/landing-page` | Profil, prestasi, anti-korupsi, SDGs, APBDes |
|
||||
| **Desa** | `/api/desa` | Berita, gallery, potensi, pengumuman, layanan |
|
||||
| **PPID** | `/api/ppid` | Semua endpoint PPID |
|
||||
| **Kesehatan** | `/api/kesehatan` | Fasilitas, puskesmas, posyandu, artikel, wabah |
|
||||
| **Ekonomi** | `/api/ekonomi` | Pasar desa, BUMDes, APBDes, pengangguran |
|
||||
| **Keamanan** | `/api/keamanan` | Keamanan, polsek, laporan, kriminalitas |
|
||||
| **Lingkungan** | `/api/lingkungan` | Sampah, penghijauan, gotong royong |
|
||||
| **Pendidikan** | `/api/pendidikan` | Sekolah, beasiswa, perpustakaan |
|
||||
| **Kependudukan** | `/api/kependudukan` | Banjar, agama, umur, migrasi |
|
||||
| **Inovasi** | `/api/inovasi` | Desa digital, kolaborasi, pengaduan |
|
||||
| **User** | `/api/admin/user` | CRUD user |
|
||||
| **Role** | `/api/admin/role` | CRUD role |
|
||||
| **Search** | `/api/search` | Global search |
|
||||
| **Utils** | `/api/utils/version` | Version info |
|
||||
|
||||
### Utility Endpoints
|
||||
|
||||
| Endpoint | Method | Deskripsi |
|
||||
|----------|--------|-----------|
|
||||
| `/api/img/:name` | GET | Serve image dengan resize |
|
||||
| `/api/img/:name` | DELETE | Delete image |
|
||||
| `/api/imgs` | GET | List images dengan pagination |
|
||||
| `/api/upl-img` | POST | Upload multiple images |
|
||||
| `/api/upl-img-single` | POST | Upload single image |
|
||||
| `/api/upl-csv` | POST | Upload CSV multiple |
|
||||
| `/api/upl-csv-single` | POST | Upload single CSV |
|
||||
|
||||
### Auth Endpoints
|
||||
|
||||
| Endpoint | Method | Deskripsi |
|
||||
|----------|--------|-----------|
|
||||
| `/api/auth/login` | POST | Login dengan OTP |
|
||||
| `/api/auth/logout` | POST | Logout |
|
||||
| `/api/auth/me` | GET | Get current user |
|
||||
|
||||
**Swagger Documentation**: Tersedia di `/api/docs`
|
||||
|
||||
---
|
||||
|
||||
## 7. Halaman Admin
|
||||
|
||||
Admin dashboard menggunakan **Mantine AppShell** dengan sidebar navigasi dinamis berbasis role.
|
||||
|
||||
### Route Group: `/admin`
|
||||
|
||||
| Section | Path | Deskripsi |
|
||||
|---------|------|-----------|
|
||||
| **Landing Page** | `/admin/landing-page/` | Profil desa, prestasi, anti-korupsi, SDGs, media sosial |
|
||||
| **Desa** | `/admin/desa/` | Berita, gallery, layanan, penghargaan, pengumuman, potensi, profil |
|
||||
| **PPID** | `/admin/ppid/` | 8 sub-modul PPID lengkap |
|
||||
| **Kesehatan** | `/admin/kesehatan/` | 8 sub-modul kesehatan |
|
||||
| **Ekonomi** | `/admin/ekonomi/` | 10 sub-modul ekonomi |
|
||||
| **Kependudukan** | `/admin/kependudukan/` | 4 sub-modul kependudukan |
|
||||
| **Pendidikan** | `/admin/pendidikan/` | 7 sub-modul pendidikan |
|
||||
| **Keamanan** | `/admin/keamanan/` | 6 sub-modul keamanan |
|
||||
| **Lingkungan** | `/admin/lingkungan/` | 6 sub-modul lingkungan |
|
||||
| **Inovasi** | `/admin/inovasi/` | 6 sub-modul inovasi |
|
||||
| **Musik** | `/admin/musik/` | Manajemen musik desa |
|
||||
| **User & Role** | `/admin/user&role/` | Manajemen user, role, menu access |
|
||||
|
||||
### Fitur Admin:
|
||||
- **Role-based Dynamic Navbar**: Navbar berubah berdasarkan roleId user
|
||||
- **Dark Mode Toggle**: Tema gelap/terang
|
||||
- **OTP Login**: Login dengan nomor telepon + kode OTP
|
||||
- **Session Management**: Multiple sessions per user dengan JWT tokens
|
||||
- **CSV Upload**: Import data via CSV
|
||||
- **Image Upload**: Upload dengan preview dan management
|
||||
- **Rich Text Editor**: Tiptap untuk konten HTML
|
||||
|
||||
### Role-Based Redirect:
|
||||
| roleId | Role | Default Redirect |
|
||||
|--------|------|-----------------|
|
||||
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
|
||||
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu` |
|
||||
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
|
||||
|
||||
---
|
||||
|
||||
## 8. Halaman Publik
|
||||
|
||||
Public website di `/darmasaba/` dengan layout yang mencakup **Navbar**, **Footer**, dan **Fixed Music Player Bar**.
|
||||
|
||||
### Route Group: `/darmasaba`
|
||||
|
||||
| Section | Path | Deskripsi |
|
||||
|---------|------|-----------|
|
||||
| **Home** | `/darmasaba` | Landing page utama |
|
||||
| **Desa** | `/darmasaba/desa` | Profil, berita, gallery, layanan, pengumuman, potensi |
|
||||
| **PPID** | `/darmasaba/ppid` | 7 sub-halaman PPID publik |
|
||||
| **Kesehatan** | `/darmasaba/kesehatan` | Info kesehatan publik |
|
||||
| **Ekonomi** | `/darmasaba/ekonomi` | Info ekonomi desa |
|
||||
| **Kependudukan** | `/darmasaba/kependudukan` | Data kependudukan |
|
||||
| **Pendidikan** | `/darmasaba/pendidikan` | Info pendidikan |
|
||||
| **Keamanan** | `/darmasaba/keamanan` | Info keamanan |
|
||||
| **Lingkungan** | `/darmasaba/lingkungan` | Info lingkungan |
|
||||
| **Inovasi** | `/darmasaba/inovasi` | Info inovasi |
|
||||
| **Musik** | `/darmasaba/musik` | Musik desa |
|
||||
| **Module** | `/darmasaba/module/*` | Link ke modul eksternal (DAVES, MANGAN, Bicara-Darma, BARES, dll) |
|
||||
|
||||
### Fitur Publik:
|
||||
- **Fixed Music Player Bar**: Player musik yang selalu tampil di bottom
|
||||
- **Global Search**: Pencarian global
|
||||
- **News Reader**: Notifikasi berita modern
|
||||
- **View Transitions**: Smooth page transitions
|
||||
- **Responsive Design**: Mobile-first dengan Mantine breakpoints
|
||||
|
||||
---
|
||||
|
||||
## 9. Komponen Utama
|
||||
|
||||
### Admin Components (`src/components/admin/`)
|
||||
|
||||
| Komponen | Deskripsi |
|
||||
|----------|-----------|
|
||||
| `AdminThemeProvider.tsx` | Theme provider untuk admin |
|
||||
| `DarkModeToggle.tsx` | Toggle dark/light mode |
|
||||
| `UnifiedSurface.tsx` | Consistent surface/card component |
|
||||
| `UnifiedTypography.tsx` | Consistent typography system |
|
||||
|
||||
### Public Shared Components (`src/app/darmasaba/_com/`)
|
||||
|
||||
| Komponen | Deskripsi |
|
||||
|----------|-----------|
|
||||
| `Navbar.tsx` | Main navigation bar |
|
||||
| `NavbarMainMenu.tsx` | Main menu dengan kategori |
|
||||
| `NavbarSubMenu.tsx` | Submenu dropdown |
|
||||
| `Footer.tsx` | Footer dengan info desa |
|
||||
| `FixedPlayerBar.tsx` | Music player bar fixed di bottom |
|
||||
| `LoadDataFirstClient.tsx` | Client-side data preloader |
|
||||
| `globalSearch.tsx` | Global search component |
|
||||
| `NewsReader.tsx` | News notification reader |
|
||||
| `ModernNewsNotification.tsx` | News toast notifications |
|
||||
|
||||
### Global Components (`src/app/_com/`)
|
||||
|
||||
| Komponen | Deskripsi |
|
||||
|----------|-----------|
|
||||
| `SpashScreen.tsx` | Splash screen on load |
|
||||
| `WebVitals.tsx` | Web Vitals monitoring |
|
||||
|
||||
---
|
||||
|
||||
## 10. State Management
|
||||
|
||||
Proyek menggunakan **multi-layer state management**:
|
||||
|
||||
| Library | Penggunaan | Lokasi |
|
||||
|---------|-----------|--------|
|
||||
| **Jotai** | Auth state (`authStore`) | `src/store/authStore.ts` |
|
||||
| **Valtio** | Dark mode, layanan, image list, nav state | `src/state/*.ts` |
|
||||
| **SWR** | Server state fetching & caching | Digunakan di components |
|
||||
| **React Context** | Music player context | `src/app/context/MusicContext.tsx` |
|
||||
| **React useState** | Local component state | Di components |
|
||||
|
||||
### State Files:
|
||||
|
||||
```
|
||||
src/state/
|
||||
darkModeStore.ts -- Valtio proxy untuk dark mode
|
||||
state-layanan.ts -- State layanan desa
|
||||
state-list-image.ts -- State list image untuk upload
|
||||
state-nav.ts -- State navigasi
|
||||
|
||||
src/store/
|
||||
authStore.ts -- Jotai atom untuk auth user state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Autentikasi
|
||||
|
||||
Sistem autentikasi menggunakan **OTP (One-Time Password)** via WhatsApp/Telepon dengan **iron-session** untuk session management.
|
||||
|
||||
### Flow Autentikasi:
|
||||
1. User memasukkan **nomor telepon** di `/login`
|
||||
2. Sistem mengirim **kode OTP** via WhatsApp Server
|
||||
3. OTP disimpan di model `KodeOtp`
|
||||
4. User memasukkan kode OTP
|
||||
5. Jika valid, session dibuat dengan **iron-session** + **JWT token**
|
||||
6. Session disimpan di `UserSession` model dengan expiry
|
||||
|
||||
### Session Structure:
|
||||
```typescript
|
||||
// src/lib/session.ts
|
||||
type SessionData = {
|
||||
user?: {
|
||||
id: string;
|
||||
name: string;
|
||||
roleId: number;
|
||||
menuIds?: string[] | null;
|
||||
isActive?: boolean;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Role-Based Access:
|
||||
| roleId | Role | Default Redirect |
|
||||
|--------|------|-----------------|
|
||||
| 0, 1, 2 | Super Admin / Admin Desa | `/admin/landing-page/profil/program-inovasi` |
|
||||
| 3 | Admin Kesehatan | `/admin/kesehatan/posyandu` |
|
||||
| 4 | Admin Pendidikan | `/admin/pendidikan/info-sekolah/jenjang-pendidikan` |
|
||||
|
||||
### Authorization:
|
||||
- **UserMenuAccess**: Mapping user ke menu yang boleh diakses
|
||||
- **Dynamic Navbar**: Navbar dirender berdasarkan `menuIds` user
|
||||
- **Inactive Users**: Dialihkan ke `/waiting-room`
|
||||
|
||||
---
|
||||
|
||||
## 12. Deployment
|
||||
|
||||
### Docker Setup
|
||||
|
||||
**Dockerfile** menggunakan **multi-stage build** dengan base image `oven/bun:1-debian`:
|
||||
|
||||
```
|
||||
Stage 1: Builder
|
||||
- Install dependencies (bun install --frozen-lockfile)
|
||||
- Generate Prisma client
|
||||
- Build Next.js (bun run build)
|
||||
|
||||
Stage 2: Runner
|
||||
- Copy .next, node_modules, public, prisma, src/lib, tsconfig.json
|
||||
- Non-root user (nextjs:nodejs)
|
||||
- Volume /app/uploads untuk file uploads
|
||||
- Port 3000
|
||||
```
|
||||
|
||||
### Entry Point (`docker-entrypoint.sh`):
|
||||
```bash
|
||||
bunx prisma migrate deploy # Run migrations
|
||||
exec bun start # Start Next.js production server
|
||||
```
|
||||
|
||||
### CI/CD dengan GitHub Actions
|
||||
|
||||
Terdapat **3 workflow**:
|
||||
|
||||
| Workflow | Trigger | Fungsi |
|
||||
|----------|---------|--------|
|
||||
| `docker-publish.yml` | Push tag `v*` | Auto build & push ke GHCR |
|
||||
| `publish.yml` | Manual (workflow_dispatch) | Build & push ke GHCR dengan input `stack_env` + `tag` |
|
||||
| `re-pull.yml` | Manual (workflow_dispatch) | Re-pull image di Portainer dengan input `stack_name` + `stack_env` |
|
||||
|
||||
### Deployment Workflow (Sequential):
|
||||
|
||||
```
|
||||
1. Update version di package.json (semver)
|
||||
2. Commit perubahan
|
||||
3. Push ke branch target (stg/prod)
|
||||
4. Trigger publish.yml:
|
||||
gh workflow run publish.yml --ref main -f stack_env=stg -f tag=<version>
|
||||
5. Tunggu sampai publish selesai (status: completed)
|
||||
6. Trigger re-pull.yml:
|
||||
gh workflow run re-pull.yml --ref main -f stack_name=desa-darmasaba -f stack_env=stg
|
||||
7. Verifikasi di Portainer
|
||||
```
|
||||
|
||||
**PENTING**: `publish.yml` dan `re-pull.yml` TIDAK boleh dijalankan bersamaan (race condition).
|
||||
|
||||
### Environments:
|
||||
- **dev**: Development
|
||||
- **stg**: Staging (`desa-darmasaba-stg.wibudev.com`)
|
||||
- **prod**: Production
|
||||
|
||||
### Notification:
|
||||
- Telegram notification via `notify.sh` script setelah setiap workflow
|
||||
|
||||
---
|
||||
|
||||
## 13. Scripts
|
||||
|
||||
| Script | Command | Deskripsi |
|
||||
|--------|---------|-----------|
|
||||
| `dev` | `next dev` | Development server |
|
||||
| `build` | `next build` | Production build |
|
||||
| `start` | `next start` | Production server |
|
||||
| `test:api` | `vitest run` | Run API unit tests |
|
||||
| `test:e2e` | `playwright test` | Run E2E tests |
|
||||
| `test` | `bun run test:api && bun run test:e2e` | Run all tests |
|
||||
| `seed` | `bun run prisma/seed.ts` | Seed database |
|
||||
| `prisma:generate` | `bunx prisma generate` | Generate Prisma client |
|
||||
| `prisma:push` | `bunx prisma db push` | Push schema to database |
|
||||
| `prisma:studio` | `bunx prisma studio` | Open Prisma Studio GUI |
|
||||
| `gen:api` | *(empty)* | Generate API types (placeholder) |
|
||||
|
||||
### Prisma Seed Configuration:
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"prisma": {
|
||||
"seed": "bun run prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Environment Variables
|
||||
|
||||
File: `.env.example`
|
||||
|
||||
| Variable | Deskripsi | Contoh |
|
||||
|----------|-----------|--------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@localhost:5432/desa-darmasaba` |
|
||||
| `SEAFILE_TOKEN` | Seafile API token | `your_seafile_token` |
|
||||
| `SEAFILE_REPO_ID` | Seafile repository ID | `your_repo_id` |
|
||||
| `SEAFILE_URL` | Seafile instance URL | `https://seafile.example.com` |
|
||||
| `SEAFILE_PUBLIC_SHARE_TOKEN` | Token untuk public share | `your_share_token` |
|
||||
| `WIBU_UPLOAD_DIR` | Upload directory path | `uploads` |
|
||||
| `WA_SERVER_TOKEN` | WhatsApp server token | `your_wa_token` |
|
||||
| `NEXT_PUBLIC_BASE_URL` | Base URL aplikasi | `/` (relative) |
|
||||
| `EMAIL_USER` | Email untuk notifikasi | `your_email@gmail.com` |
|
||||
| `EMAIL_PASS` | Email app password | `your_app_password` |
|
||||
| `BASE_TOKEN_KEY` | JWT secret key | `your_jwt_secret` |
|
||||
| `BOT_TOKEN` | Telegram bot token | `your_bot_token` |
|
||||
| `CHAT_ID` | Telegram chat ID | `your_chat_id` |
|
||||
| `SESSION_PASSWORD` | iron-session password (min 32 chars) | `secure_32_char_password` |
|
||||
| `ELEVENLABS_API_KEY` | ElevenLabs API (TTS - optional) | `your_elevenlabs_key` |
|
||||
|
||||
---
|
||||
|
||||
## 15. Layanan Eksternal
|
||||
|
||||
### PostgreSQL
|
||||
- **Provider**: PostgreSQL via Prisma ORM
|
||||
- **Schema**: `public`
|
||||
- **Connection**: Via `DATABASE_URL` environment variable
|
||||
- **Migrations**: `prisma migrate deploy` di docker entrypoint
|
||||
|
||||
### Seafile (File Storage)
|
||||
- **Tipe**: Self-hosted file sync & share
|
||||
- **Penggunaan**: Storage untuk images, documents, audio files
|
||||
- **Integrasi**: `src/lib/seafile-auth-service.ts`
|
||||
- **CDN**: URL generation untuk public sharing
|
||||
- **Config**: Token, repo ID, base URL
|
||||
|
||||
### WhatsApp Server
|
||||
- **Penggunaan**: Kirim OTP codes saat login
|
||||
- **Config**: `WA_SERVER_TOKEN`
|
||||
|
||||
### Telegram Bot
|
||||
- **Penggunaan**: Notifikasi deployment & sistem
|
||||
- **Config**: `BOT_TOKEN` + `CHAT_ID`
|
||||
- **Integration**: `notify.sh` script di GitHub Actions
|
||||
|
||||
### ElevenLabs (Optional)
|
||||
- **Penggunaan**: Text-to-Speech (TTS) features
|
||||
- **Config**: `ELEVENLABS_API_KEY`
|
||||
|
||||
### Email (Nodemailer)
|
||||
- **Penggunaan**: Notifikasi email untuk subscription/pengumuman
|
||||
- **Config**: `EMAIL_USER` + `EMAIL_PASS`
|
||||
- **Provider**: Gmail (app password)
|
||||
|
||||
---
|
||||
|
||||
## Ringkasan Cepat
|
||||
|
||||
| Aspek | Detail |
|
||||
|-------|--------|
|
||||
| **Framework** | Next.js 15 (App Router) + Elysia.js |
|
||||
| **Database** | PostgreSQL + Prisma (100+ models) |
|
||||
| **Auth** | OTP + iron-session + JWT |
|
||||
| **Storage** | Seafile + local uploads |
|
||||
| **UI** | Mantine UI + Tiptap + Framer Motion |
|
||||
| **State** | Jotai + Valtio + SWR |
|
||||
| **Deploy** | Docker + GHCR + Portainer + GitHub Actions |
|
||||
| **Runtime** | Bun |
|
||||
| **Testing** | Vitest + Playwright |
|
||||
| **Version** | 0.1.11 |
|
||||
30
__tests__/api/fileStorage.test.ts
Normal file
30
__tests__/api/fileStorage.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
describe('FileStorage API', () => {
|
||||
it('should fetch a list of files from /api/fileStorage/findMany', async () => {
|
||||
const response = await ApiFetch.api.fileStorage.findMany.get();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const responseBody = response.data as any;
|
||||
|
||||
expect(responseBody.data).toBeInstanceOf(Array);
|
||||
expect(responseBody.data.length).toBe(2);
|
||||
expect(responseBody.data[0].name).toBe('file1.jpg');
|
||||
});
|
||||
|
||||
it('should create a file using /api/fileStorage/create', async () => {
|
||||
const mockFile = new File(['hello'], 'hello.png', { type: 'image/png' });
|
||||
const response = await ApiFetch.api.fileStorage.create.post({
|
||||
file: mockFile,
|
||||
name: 'hello.png',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const responseBody = response.data as any;
|
||||
|
||||
expect(responseBody.data.realName).toBe('hello.png');
|
||||
expect(responseBody.data.id).toBe('3');
|
||||
});
|
||||
});
|
||||
11
__tests__/e2e/homepage.spec.ts
Normal file
11
__tests__/e2e/homepage.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('homepage has correct title and content', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Wait for the redirect to /darmasaba
|
||||
await page.waitForURL('/darmasaba');
|
||||
|
||||
// Check for the main heading
|
||||
await expect(page.getByText('DARMASABA', { exact: true })).toBeVisible();
|
||||
});
|
||||
43
__tests__/mocks/handlers.ts
Normal file
43
__tests__/mocks/handlers.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const handlers = [
|
||||
http.get('http://localhost:3000/api/fileStorage/findMany', () => {
|
||||
return HttpResponse.json({
|
||||
data: [
|
||||
{ id: '1', name: 'file1.jpg', url: '/uploads/file1.jpg' },
|
||||
{ id: '2', name: 'file2.png', url: '/uploads/file2.png' },
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 2,
|
||||
totalPages: 1,
|
||||
},
|
||||
});
|
||||
}),
|
||||
http.post('http://localhost:3000/api/fileStorage/create', async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const file = data.get('file') as File;
|
||||
const name = data.get('name') as string;
|
||||
|
||||
if (!file) {
|
||||
return new HttpResponse(null, { status: 400 });
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
id: '3',
|
||||
name: 'generated-nanoid',
|
||||
path: `/uploads/generated-nanoid`,
|
||||
link: `/uploads/generated-nanoid`,
|
||||
realName: name,
|
||||
mimeType: file.type,
|
||||
category: "uncategorized",
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
}
|
||||
});
|
||||
}),
|
||||
];
|
||||
4
__tests__/mocks/server.ts
Normal file
4
__tests__/mocks/server.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { handlers } from './handlers';
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
7
__tests__/setup.ts
Normal file
7
__tests__/setup.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { server } from './mocks/server';
|
||||
import { beforeAll, afterEach, afterAll } from 'vitest';
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
49
biome.json
Normal file
49
biome.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"experimentalScannerIgnores": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"out",
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"noUnusedVariables": "warn",
|
||||
"noUnusedImports": "warn"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always"
|
||||
}
|
||||
}
|
||||
}
|
||||
169
darkMode.md
Normal file
169
darkMode.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 🌙 Dark Mode Design Specification
|
||||
## Admin Darmasaba – Dashboard & CMS
|
||||
|
||||
Dokumen ini mendefinisikan standar **Dark Mode UI** agar:
|
||||
- nyaman di mata
|
||||
- konsisten
|
||||
- tidak flat
|
||||
- tetap profesional untuk aplikasi pemerintahan
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Color Palette (Dark Mode)
|
||||
|
||||
### Background Layers
|
||||
| Layer | Token | Warna | Fungsi |
|
||||
|------|------|------|------|
|
||||
| Base | `--bg-base` | `#0B1220` | Background utama aplikasi |
|
||||
| App | `--bg-app` | `#0F172A` | Area kerja utama |
|
||||
| Card | `--bg-card` | `#162235` | Card / container |
|
||||
| Surface | `--bg-surface` | `#1E2A3D` | Table header, tab, input |
|
||||
|
||||
---
|
||||
|
||||
### Border & Divider
|
||||
| Token | Warna | Catatan |
|
||||
|-----|------|--------|
|
||||
| `--border-default` | `#2A3A52` | Border utama |
|
||||
| `--border-soft` | `#22314A` | Divider halus |
|
||||
|
||||
> ❗ Hindari border terlalu tipis (`opacity < 20%`)
|
||||
|
||||
---
|
||||
|
||||
### Text Colors
|
||||
| Jenis | Token | Warna |
|
||||
|-----|------|------|
|
||||
| Primary | `--text-primary` | `#E5E7EB` |
|
||||
| Secondary | `--text-secondary` | `#9CA3AF` |
|
||||
| Muted | `--text-muted` | `#6B7280` |
|
||||
| Inverse | `--text-inverse` | `#020617` |
|
||||
|
||||
---
|
||||
|
||||
### Accent & Action
|
||||
| Fungsi | Warna |
|
||||
|------|------|
|
||||
| Primary Action | `#3B82F6` |
|
||||
| Hover | `#2563EB` |
|
||||
| Active | `#1D4ED8` |
|
||||
| Link | `#60A5FA` |
|
||||
|
||||
---
|
||||
|
||||
### Status Colors
|
||||
| Status | Warna |
|
||||
|------|------|
|
||||
| Success | `#22C55E` |
|
||||
| Warning | `#FACC15` |
|
||||
| Error | `#EF4444` |
|
||||
| Info | `#38BDF8` |
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Layout Rules
|
||||
|
||||
### Sidebar
|
||||
- Background: `--bg-app`
|
||||
- Active menu:
|
||||
- Background: `rgba(59,130,246,0.15)`
|
||||
- Text: Primary
|
||||
- Indicator: kiri (2–3px accent bar)
|
||||
- Hover:
|
||||
- Background: `rgba(255,255,255,0.04)`
|
||||
|
||||
---
|
||||
|
||||
### Header / Topbar
|
||||
- Background: `linear-gradient(#0F172A → #0B1220)`
|
||||
- Border bawah wajib (`--border-soft`)
|
||||
- Icon:
|
||||
- Default: muted
|
||||
- Hover: primary
|
||||
|
||||
---
|
||||
|
||||
## 📦 Card & Section
|
||||
|
||||
### Card
|
||||
- Background: `--bg-card`
|
||||
- Border: `--border-default`
|
||||
- Radius: 12–16px
|
||||
- Jangan pakai shadow hitam
|
||||
|
||||
### Section Header
|
||||
- Font weight lebih besar
|
||||
- Text: primary
|
||||
- Spacing jelas dari konten
|
||||
|
||||
---
|
||||
|
||||
## 📊 Table (Dark Mode Friendly)
|
||||
|
||||
### Table Header
|
||||
- Background: `--bg-surface`
|
||||
- Text: secondary
|
||||
- Font weight: medium
|
||||
|
||||
### Table Row
|
||||
- Default: transparent
|
||||
- Hover:
|
||||
- Background: `rgba(255,255,255,0.03)`
|
||||
- Divider antar row wajib terlihat
|
||||
|
||||
### Link di Table
|
||||
- Warna link **lebih terang dari text**
|
||||
- Hover underline
|
||||
|
||||
---
|
||||
|
||||
## 🔘 Button Rules
|
||||
|
||||
### Primary Button
|
||||
- Background: Primary Action
|
||||
- Text: Inverse
|
||||
- Hover: darker shade
|
||||
|
||||
### Secondary Button
|
||||
- Background: transparent
|
||||
- Border: `--border-default`
|
||||
- Text: primary
|
||||
|
||||
### Icon Button
|
||||
- Default: muted
|
||||
- Hover: primary + bg soft
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Tab Navigation
|
||||
|
||||
- Inactive:
|
||||
- Text: muted
|
||||
- Active:
|
||||
- Background: `rgba(59,130,246,0.15)`
|
||||
- Text: primary
|
||||
- Icon ikut berubah
|
||||
|
||||
---
|
||||
|
||||
## 🌗 Dark vs Light Mode Rule
|
||||
- Layout, spacing, typography **HARUS SAMA**
|
||||
- Yang boleh beda:
|
||||
- warna
|
||||
- border intensity
|
||||
- background layer
|
||||
|
||||
> ❌ Jangan ganti struktur UI antara dark & light
|
||||
|
||||
---
|
||||
|
||||
## ✅ Dark Mode Checklist
|
||||
- [ ] Kontras teks terbaca
|
||||
- [ ] Active state jelas
|
||||
- [ ] Hover terasa hidup
|
||||
- [ ] Tidak flat
|
||||
- [ ] Tidak silau
|
||||
|
||||
---
|
||||
|
||||
Dokumen ini adalah **single source of truth** untuk Dark Mode.
|
||||
553
dev-inspector-click-to-source.md
Normal file
553
dev-inspector-click-to-source.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# Skill: Dev Inspector — Click-to-Source untuk Bun + Elysia + Vite + React
|
||||
|
||||
## Ringkasan
|
||||
|
||||
Fitur development: klik elemen UI di browser → langsung buka source code di editor (VS Code, Cursor, dll) pada baris dan kolom yang tepat. Zero overhead di production.
|
||||
|
||||
**Hotkey**: `Ctrl+Shift+Cmd+C` (macOS) / `Ctrl+Shift+Alt+C` → aktifkan mode inspect → klik elemen → file terbuka.
|
||||
|
||||
## Kenapa Tidak Pakai Library
|
||||
|
||||
`react-dev-inspector` crash di React 19 karena:
|
||||
- `fiber.return.child.sibling` bisa null di React 19
|
||||
- `_debugSource` dihapus dari React 19
|
||||
- Walking fiber tree tidak stabil antar versi React
|
||||
|
||||
Solusi ini **regex-based + multi-fallback**, tidak bergantung pada React internals.
|
||||
|
||||
## Syarat Arsitektur
|
||||
|
||||
Fitur ini bekerja karena 4 syarat struktural terpenuhi. Jika salah satu tidak ada, fitur tidak bisa diimplementasi atau perlu adaptasi signifikan.
|
||||
|
||||
### 1. Vite sebagai Bundler (Wajib)
|
||||
|
||||
Seluruh mekanisme bergantung pada **Vite plugin transform pipeline**:
|
||||
- `inspectorPlugin()` inject attributes ke JSX saat build/HMR
|
||||
- `enforce: 'pre'` memastikan plugin jalan sebelum OXC/Babel transform JSX
|
||||
- `import.meta.env?.DEV` sebagai compile-time constant untuk tree-shaking
|
||||
|
||||
**Tidak bisa diganti dengan**: esbuild standalone, webpack (perlu loader berbeda), SWC standalone.
|
||||
**Bisa diganti dengan**: framework yang pakai Vite di dalamnya (Remix Vite, TanStack Start, Astro).
|
||||
|
||||
### 2. Server dan Frontend dalam Satu Proses (Wajib)
|
||||
|
||||
Endpoint `/__open-in-editor` harus **satu proses dengan dev server** yang melayani frontend:
|
||||
- Browser POST ke origin yang sama (no CORS)
|
||||
- Server punya akses ke filesystem lokal untuk `Bun.spawn(editor)`
|
||||
- Endpoint harus bisa ditangani **sebelum routing & middleware** (auth, tenant, dll)
|
||||
|
||||
**Pola yang memenuhi syarat:**
|
||||
- Elysia + Vite middlewareMode (project ini) — `onRequest` intercept sebelum route matching
|
||||
- Express/Fastify + Vite middlewareMode — middleware biasa sebelum auth
|
||||
- Vite dev server standalone (`vite dev`) — pakai `configureServer` hook
|
||||
|
||||
**Tidak memenuhi syarat:**
|
||||
- Frontend dan backend di proses/port terpisah (misal: CRA + separate API server) — perlu proxy atau CORS config tambahan
|
||||
- Serverless/edge deployment — tidak bisa `spawn` editor
|
||||
|
||||
### 3. React sebagai UI Framework (Wajib untuk Multi-Fallback)
|
||||
|
||||
Strategi extraction source info bergantung pada React internals:
|
||||
1. `__reactProps$*` — React menyimpan props di DOM element
|
||||
2. `__reactFiber$*` — React fiber tree untuk walk-up
|
||||
3. DOM attribute — fallback universal
|
||||
|
||||
**Jika pakai framework lain** (Vue, Svelte, Solid):
|
||||
- Hanya strategi 3 (DOM attribute) yang berfungsi — tetap cukup
|
||||
- Hapus strategi 1 & 2 dari `getCodeInfoFromElement()`
|
||||
- Inject attributes tetap via Vite plugin (framework-agnostic)
|
||||
|
||||
### 4. Bun sebagai Runtime (Direkomendasikan, Bukan Wajib)
|
||||
|
||||
Bun memberikan API yang lebih clean:
|
||||
- `Bun.spawn()` — fire-and-forget tanpa import
|
||||
- `Bun.which()` — cek executable ada di PATH (mencegah uncatchable error)
|
||||
|
||||
**Jika pakai Node.js:**
|
||||
- `Bun.spawn()` → `child_process.spawn(editor, args, { detached: true, stdio: 'ignore' }).unref()`
|
||||
- `Bun.which()` → `const which = require('which'); which.sync(editor, { nothrow: true })`
|
||||
|
||||
### Ringkasan Syarat
|
||||
|
||||
| Syarat | Wajib? | Alternatif |
|
||||
|-------------------------------|----------|------------------------------------------------------|
|
||||
| Vite sebagai bundler | Ya | Framework berbasis Vite (Remix, Astro, dll) |
|
||||
| Server + frontend satu proses | Ya | Bisa diakali dengan proxy, tapi tambah kompleksitas |
|
||||
| React | Sebagian | Framework lain bisa, hanya fallback ke DOM attribute |
|
||||
| Bun runtime | Tidak | Node.js dengan `child_process` + `which` package |
|
||||
|
||||
## Arsitektur
|
||||
|
||||
```
|
||||
BUILD TIME (Vite Plugin):
|
||||
.tsx/.jsx file
|
||||
→ [inspectorPlugin enforce:'pre'] inject data-inspector-* attributes ke JSX
|
||||
→ [react() OXC] transform JSX ke createElement
|
||||
→ Browser menerima elemen dengan attributes
|
||||
|
||||
RUNTIME (Browser):
|
||||
Hotkey → aktifkan mode → hover elemen → baca attributes → klik
|
||||
→ POST /__open-in-editor {relativePath, line, column}
|
||||
|
||||
BACKEND (Elysia onRequest):
|
||||
/__open-in-editor → Bun.spawn([editor, '--goto', 'file:line:col'])
|
||||
→ Editor terbuka di lokasi tepat
|
||||
```
|
||||
|
||||
## Komponen yang Dibutuhkan
|
||||
|
||||
### 1. Vite Plugin — `inspectorPlugin()` (enforce: 'pre')
|
||||
|
||||
Inject `data-inspector-*` ke setiap JSX opening tag via regex.
|
||||
|
||||
**HARUS `enforce: 'pre'`** — kalau tidak, OXC transform JSX duluan dan regex tidak bisa menemukan `<Component`.
|
||||
|
||||
```typescript
|
||||
// Taruh di file vite config (misal: src/vite.ts atau vite.config.ts)
|
||||
import path from 'node:path'
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
function inspectorPlugin(): Plugin {
|
||||
const rootDir = process.cwd()
|
||||
|
||||
return {
|
||||
name: 'inspector-inject',
|
||||
enforce: 'pre',
|
||||
transform(code, id) {
|
||||
// Hanya .tsx/.jsx, skip node_modules
|
||||
if (!/\.[jt]sx(\?|$)/.test(id) || id.includes('node_modules')) return null
|
||||
if (!code.includes('<')) return null
|
||||
|
||||
const relativePath = path.relative(rootDir, id)
|
||||
let modified = false
|
||||
const lines = code.split('\n')
|
||||
const result: string[] = []
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i]
|
||||
// Match JSX opening tags: <Component atau <div
|
||||
// Skip TypeScript generics (Record<string>) via charBefore check
|
||||
const jsxPattern = /(<(?:[A-Z][a-zA-Z0-9.]*|[a-z][a-zA-Z0-9-]*))\b/g
|
||||
let match: RegExpExecArray | null = null
|
||||
|
||||
while ((match = jsxPattern.exec(line)) !== null) {
|
||||
// Skip jika karakter sebelum `<` adalah identifier char (TypeScript generic)
|
||||
const charBefore = match.index > 0 ? line[match.index - 1] : ''
|
||||
if (/[a-zA-Z0-9_$.]/.test(charBefore)) continue
|
||||
|
||||
const col = match.index + 1
|
||||
const attr = ` data-inspector-line="${i + 1}" data-inspector-column="${col}" data-inspector-relative-path="${relativePath}"`
|
||||
const insertPos = match.index + match[0].length
|
||||
line = line.slice(0, insertPos) + attr + line.slice(insertPos)
|
||||
modified = true
|
||||
jsxPattern.lastIndex += attr.length
|
||||
}
|
||||
|
||||
result.push(line)
|
||||
}
|
||||
|
||||
if (!modified) return null
|
||||
return result.join('\n')
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Mengapa regex, bukan Babel?**
|
||||
- `@vitejs/plugin-react` v6+ pakai OXC (Rust), bukan Babel
|
||||
- Config `babel: { plugins: [...] }` di plugin-react **DIABAIKAN**
|
||||
- Regex jalan sebelum OXC via `enforce: 'pre'`
|
||||
|
||||
**Gotcha: TypeScript generics**
|
||||
- `Record<string>` → karakter sebelum `<` adalah `d` (identifier) → SKIP
|
||||
- `<Button` → karakter sebelum `<` adalah space/newline → MATCH
|
||||
|
||||
### 2. Vite Plugin Order (KRITIS)
|
||||
|
||||
```typescript
|
||||
plugins: [
|
||||
// 1. Route generation (jika pakai TanStack Router)
|
||||
TanStackRouterVite({ ... }),
|
||||
|
||||
// 2. Inspector inject — HARUS sebelum react()
|
||||
inspectorPlugin(),
|
||||
|
||||
// 3. React OXC transform
|
||||
react(),
|
||||
|
||||
// 4. (Opsional) Dedupe React Refresh untuk middlewareMode
|
||||
dedupeRefreshPlugin(),
|
||||
]
|
||||
```
|
||||
|
||||
**Jika urutan salah (inspectorPlugin setelah react):**
|
||||
- OXC transform `<Button>` → `React.createElement(Button, ...)`
|
||||
- Regex tidak menemukan `<Button` → attributes TIDAK ter-inject
|
||||
- Fitur tidak berfungsi, tanpa error
|
||||
|
||||
### 3. DevInspector Component (Browser Runtime)
|
||||
|
||||
Komponen React yang handle hotkey, overlay, dan klik.
|
||||
|
||||
```tsx
|
||||
// src/frontend/DevInspector.tsx
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface CodeInfo {
|
||||
relativePath: string
|
||||
line: string
|
||||
column: string
|
||||
}
|
||||
|
||||
/** Baca data-inspector-* dari fiber props atau DOM attributes */
|
||||
function getCodeInfoFromElement(element: HTMLElement): CodeInfo | null {
|
||||
// Strategi 1: React internal props __reactProps$ (paling akurat)
|
||||
for (const key of Object.keys(element)) {
|
||||
if (key.startsWith('__reactProps$')) {
|
||||
const props = (element as any)[key]
|
||||
if (props?.['data-inspector-relative-path']) {
|
||||
return {
|
||||
relativePath: props['data-inspector-relative-path'],
|
||||
line: props['data-inspector-line'] || '1',
|
||||
column: props['data-inspector-column'] || '1',
|
||||
}
|
||||
}
|
||||
}
|
||||
// Strategi 2: Walk fiber tree __reactFiber$
|
||||
if (key.startsWith('__reactFiber$')) {
|
||||
const fiber = (element as any)[key]
|
||||
let f = fiber
|
||||
while (f) {
|
||||
const p = f.pendingProps || f.memoizedProps
|
||||
if (p?.['data-inspector-relative-path']) {
|
||||
return {
|
||||
relativePath: p['data-inspector-relative-path'],
|
||||
line: p['data-inspector-line'] || '1',
|
||||
column: p['data-inspector-column'] || '1',
|
||||
}
|
||||
}
|
||||
// Fallback: _debugSource (React < 19)
|
||||
const src = f._debugSource ?? f._debugOwner?._debugSource
|
||||
if (src?.fileName && src?.lineNumber) {
|
||||
return {
|
||||
relativePath: src.fileName,
|
||||
line: String(src.lineNumber),
|
||||
column: String(src.columnNumber ?? 1),
|
||||
}
|
||||
}
|
||||
f = f.return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategi 3: Fallback DOM attribute langsung
|
||||
const rp = element.getAttribute('data-inspector-relative-path')
|
||||
if (rp) {
|
||||
return {
|
||||
relativePath: rp,
|
||||
line: element.getAttribute('data-inspector-line') || '1',
|
||||
column: element.getAttribute('data-inspector-column') || '1',
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** Walk up DOM tree sampai ketemu elemen yang punya source info */
|
||||
function findCodeInfo(target: HTMLElement): CodeInfo | null {
|
||||
let el: HTMLElement | null = target
|
||||
while (el) {
|
||||
const info = getCodeInfoFromElement(el)
|
||||
if (info) return info
|
||||
el = el.parentElement
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function openInEditor(info: CodeInfo) {
|
||||
fetch('/__open-in-editor', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
relativePath: info.relativePath,
|
||||
lineNumber: info.line,
|
||||
columnNumber: info.column,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export function DevInspector({ children }: { children: React.ReactNode }) {
|
||||
const [active, setActive] = useState(false)
|
||||
const overlayRef = useRef<HTMLDivElement | null>(null)
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null)
|
||||
const lastInfoRef = useRef<CodeInfo | null>(null)
|
||||
|
||||
const updateOverlay = useCallback((target: HTMLElement | null) => {
|
||||
const ov = overlayRef.current
|
||||
const tt = tooltipRef.current
|
||||
if (!ov || !tt) return
|
||||
|
||||
if (!target) {
|
||||
ov.style.display = 'none'
|
||||
tt.style.display = 'none'
|
||||
lastInfoRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
const info = findCodeInfo(target)
|
||||
if (!info) {
|
||||
ov.style.display = 'none'
|
||||
tt.style.display = 'none'
|
||||
lastInfoRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
lastInfoRef.current = info
|
||||
|
||||
const rect = target.getBoundingClientRect()
|
||||
ov.style.display = 'block'
|
||||
ov.style.top = `${rect.top + window.scrollY}px`
|
||||
ov.style.left = `${rect.left + window.scrollX}px`
|
||||
ov.style.width = `${rect.width}px`
|
||||
ov.style.height = `${rect.height}px`
|
||||
|
||||
tt.style.display = 'block'
|
||||
tt.textContent = `${info.relativePath}:${info.line}`
|
||||
const ttTop = rect.top + window.scrollY - 24
|
||||
tt.style.top = `${ttTop > 0 ? ttTop : rect.bottom + window.scrollY + 4}px`
|
||||
tt.style.left = `${rect.left + window.scrollX}px`
|
||||
}, [])
|
||||
|
||||
// Activate/deactivate event listeners
|
||||
useEffect(() => {
|
||||
if (!active) return
|
||||
|
||||
const onMouseOver = (e: MouseEvent) => updateOverlay(e.target as HTMLElement)
|
||||
|
||||
const onClick = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const info = lastInfoRef.current ?? findCodeInfo(e.target as HTMLElement)
|
||||
if (info) {
|
||||
const loc = `${info.relativePath}:${info.line}:${info.column}`
|
||||
console.log('[DevInspector] Open:', loc)
|
||||
navigator.clipboard.writeText(loc)
|
||||
openInEditor(info)
|
||||
}
|
||||
setActive(false)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setActive(false)
|
||||
}
|
||||
|
||||
document.addEventListener('mouseover', onMouseOver, true)
|
||||
document.addEventListener('click', onClick, true)
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
document.body.style.cursor = 'crosshair'
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseover', onMouseOver, true)
|
||||
document.removeEventListener('click', onClick, true)
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
document.body.style.cursor = ''
|
||||
if (overlayRef.current) overlayRef.current.style.display = 'none'
|
||||
if (tooltipRef.current) tooltipRef.current.style.display = 'none'
|
||||
}
|
||||
}, [active, updateOverlay])
|
||||
|
||||
// Hotkey: Ctrl+Shift+Cmd+C (macOS) / Ctrl+Shift+Alt+C
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key.toLowerCase() === 'c' && e.ctrlKey && e.shiftKey && (e.metaKey || e.altKey)) {
|
||||
e.preventDefault()
|
||||
setActive((prev) => !prev)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
return () => document.removeEventListener('keydown', onKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<div
|
||||
ref={overlayRef}
|
||||
style={{
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
border: '2px solid #3b82f6',
|
||||
backgroundColor: 'rgba(59,130,246,0.1)',
|
||||
zIndex: 99999,
|
||||
transition: 'all 0.05s ease',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
style={{
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: '#1e293b',
|
||||
color: '#e2e8f0',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
zIndex: 100000,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Backend Endpoint — `/__open-in-editor`
|
||||
|
||||
**HARUS ditangani di `onRequest` / sebelum middleware**, bukan sebagai route biasa. Kalau jadi route, akan kena auth middleware dan gagal.
|
||||
|
||||
```typescript
|
||||
// Di entry point server (src/index.tsx), dalam onRequest handler:
|
||||
|
||||
if (!isProduction && pathname === '/__open-in-editor' && request.method === 'POST') {
|
||||
const { relativePath, lineNumber, columnNumber } = (await request.json()) as {
|
||||
relativePath: string
|
||||
lineNumber: string
|
||||
columnNumber: string
|
||||
}
|
||||
const file = `${process.cwd()}/${relativePath}`
|
||||
const editor = process.env.REACT_EDITOR || 'code'
|
||||
const loc = `${file}:${lineNumber}:${columnNumber}`
|
||||
const args = editor === 'subl' ? [loc] : ['--goto', loc]
|
||||
const editorPath = Bun.which(editor)
|
||||
console.log(`[inspector] ${editor} → ${editorPath ?? 'NOT FOUND'} → ${loc}`)
|
||||
if (editorPath) {
|
||||
Bun.spawn([editor, ...args], { stdio: ['ignore', 'ignore', 'ignore'] })
|
||||
} else {
|
||||
console.error(`[inspector] Editor "${editor}" not found in PATH. Set REACT_EDITOR in .env`)
|
||||
}
|
||||
return new Response('ok')
|
||||
}
|
||||
```
|
||||
|
||||
**Penting — `Bun.which()` sebelum `Bun.spawn()`:**
|
||||
- `Bun.spawn()` throw native error yang TIDAK bisa di-catch jika executable tidak ada
|
||||
- `Bun.which()` return null dengan aman → cek dulu sebelum spawn
|
||||
|
||||
**Editor yang didukung:**
|
||||
|
||||
| REACT_EDITOR | Editor | Args |
|
||||
|------------------|--------------|--------------------------------|
|
||||
| `code` (default) | VS Code | `--goto file:line:col` |
|
||||
| `cursor` | Cursor | `--goto file:line:col` |
|
||||
| `windsurf` | Windsurf | `--goto file:line:col` |
|
||||
| `subl` | Sublime Text | `file:line:col` (tanpa --goto) |
|
||||
|
||||
### 5. Frontend Entry — Conditional Import (Zero Production Overhead)
|
||||
|
||||
```tsx
|
||||
// src/frontend.tsx (atau entry point React)
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
const InspectorWrapper = import.meta.env?.DEV
|
||||
? (await import('./frontend/DevInspector')).DevInspector
|
||||
: ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
|
||||
const app = (
|
||||
<InspectorWrapper>
|
||||
<App />
|
||||
</InspectorWrapper>
|
||||
)
|
||||
```
|
||||
|
||||
**Bagaimana zero overhead tercapai:**
|
||||
- `import.meta.env?.DEV` adalah compile-time constant
|
||||
- Production build: `false` → dynamic import TIDAK dieksekusi
|
||||
- Tree-shaking menghapus seluruh `DevInspector.tsx` dari bundle
|
||||
- Tidak ada runtime check, tidak ada dead code di bundle
|
||||
|
||||
### 6. (Opsional) Dedupe React Refresh — Workaround Vite middlewareMode
|
||||
|
||||
Jika pakai Vite dalam `middlewareMode` (seperti di Elysia/Express), `@vitejs/plugin-react` v6 bisa inject React Refresh footer dua kali → error "already declared".
|
||||
|
||||
```typescript
|
||||
function dedupeRefreshPlugin(): Plugin {
|
||||
return {
|
||||
name: 'dedupe-react-refresh',
|
||||
enforce: 'post',
|
||||
transform(code, id) {
|
||||
if (!/\.[jt]sx(\?|$)/.test(id) || id.includes('node_modules')) return null
|
||||
|
||||
const marker = 'import * as RefreshRuntime from "/@react-refresh"'
|
||||
const firstIdx = code.indexOf(marker)
|
||||
if (firstIdx === -1) return null
|
||||
|
||||
const secondIdx = code.indexOf(marker, firstIdx + marker.length)
|
||||
if (secondIdx === -1) return null
|
||||
|
||||
const sourcemapIdx = code.indexOf('\n//# sourceMappingURL=', secondIdx)
|
||||
const endIdx = sourcemapIdx !== -1 ? sourcemapIdx : code.length
|
||||
|
||||
const cleaned = code.slice(0, secondIdx) + code.slice(endIdx)
|
||||
return { code: cleaned, map: null }
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Langkah Implementasi di Project Baru
|
||||
|
||||
### Prasyarat
|
||||
- Runtime: Bun
|
||||
- Server: Elysia (atau framework lain dengan onRequest/beforeHandle)
|
||||
- Frontend: React + Vite
|
||||
- `@vitejs/plugin-react` (OXC)
|
||||
|
||||
### Step-by-step
|
||||
|
||||
1. **Buat `DevInspector.tsx`** — copy komponen dari Bagian 3 ke folder frontend
|
||||
2. **Tambah `inspectorPlugin()`** — copy fungsi dari Bagian 1 ke file vite config
|
||||
3. **Atur plugin order** — `inspectorPlugin()` SEBELUM `react()` (Bagian 2)
|
||||
4. **Tambah endpoint `/__open-in-editor`** — di `onRequest` handler (Bagian 4)
|
||||
5. **Wrap root app** — conditional import di entry point (Bagian 5)
|
||||
6. **Set env** — `REACT_EDITOR=code` (atau cursor/windsurf/subl) di `.env`
|
||||
7. **(Opsional)** Tambah `dedupeRefreshPlugin()` jika pakai Vite `middlewareMode`
|
||||
|
||||
### Checklist Verifikasi
|
||||
|
||||
- [ ] `inspectorPlugin` punya `enforce: 'pre'`
|
||||
- [ ] Plugin order: inspector → react (bukan sebaliknya)
|
||||
- [ ] Endpoint `/__open-in-editor` di LUAR middleware auth
|
||||
- [ ] `Bun.which(editor)` dipanggil SEBELUM `Bun.spawn()`
|
||||
- [ ] Conditional import pakai `import.meta.env?.DEV`
|
||||
- [ ] `REACT_EDITOR` di `.env` sesuai editor yang dipakai
|
||||
- [ ] Hotkey berfungsi: `Ctrl+Shift+Cmd+C` / `Ctrl+Shift+Alt+C`
|
||||
|
||||
## Gotcha & Pelajaran
|
||||
|
||||
| Masalah | Penyebab | Solusi |
|
||||
|----------------------------------|---------------------------------------------|-----------------------------------------------|
|
||||
| Attributes tidak ter-inject | Plugin order salah | `enforce: 'pre'`, taruh sebelum `react()` |
|
||||
| `Record<string>` ikut ter-inject | Regex match TypeScript generics | Cek `charBefore` — skip jika identifier char |
|
||||
| `Bun.spawn` crash | Editor tidak ada di PATH | Selalu `Bun.which()` dulu |
|
||||
| Hotkey tidak response | `e.key` return 'C' (uppercase) karena Shift | Pakai `e.key.toLowerCase()` |
|
||||
| React Refresh duplicate | Vite middlewareMode bug | `dedupeRefreshPlugin()` enforce: 'post' |
|
||||
| Endpoint kena auth middleware | Didaftarkan sebagai route biasa | Tangani di `onRequest` sebelum routing |
|
||||
| `_debugSource` undefined | React 19 menghapusnya | Multi-fallback: reactProps → fiber → DOM attr |
|
||||
|
||||
## Adaptasi untuk Framework Lain
|
||||
|
||||
### Express/Fastify (bukan Elysia)
|
||||
- Endpoint `/__open-in-editor`: gunakan middleware biasa SEBELUM auth
|
||||
- `Bun.spawn` → `child_process.spawn` jika pakai Node.js
|
||||
- `Bun.which` → `which` npm package jika pakai Node.js
|
||||
|
||||
### Next.js
|
||||
- Tidak perlu — Next.js punya built-in click-to-source
|
||||
- Tapi jika ingin custom: taruh endpoint di `middleware.ts`, plugin di `next.config.js`
|
||||
|
||||
### Remix/Tanstack Start (SSR)
|
||||
- Plugin tetap sama (Vite-based)
|
||||
- Endpoint perlu di server entry, bukan di route loader
|
||||
13
docker-entrypoint.sh
Normal file
13
docker-entrypoint.sh
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔄 Running database migrations..."
|
||||
cd /app
|
||||
bunx prisma migrate deploy || {
|
||||
echo "❌ Migration failed!"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Migrations completed successfully"
|
||||
|
||||
echo "🚀 Starting application..."
|
||||
exec bun start
|
||||
@@ -11,6 +11,11 @@ const compat = new FlatCompat({
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
99
gambar.ttx
Normal file
99
gambar.ttx
Normal file
@@ -0,0 +1,99 @@
|
||||
type DirItem = {
|
||||
type: "file" | "dir";
|
||||
name: string;
|
||||
path: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
// type FileDownloadResponse = {
|
||||
// url: string;
|
||||
// };
|
||||
|
||||
// const TOKEN = "20a19f4a04032215d50ce53292e6abdd38b9f806";
|
||||
// const REPO_ID = "8814bfe1-30d5-4e77-ab36-3122fa59a022";
|
||||
// const DIR_TARGET = "image";
|
||||
|
||||
// const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
|
||||
|
||||
const TOKEN = process.env.SEAFILE_TOKEN!;
|
||||
const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
||||
|
||||
// ⛔ PENTING: RELATIVE PATH (tanpa slash depan)
|
||||
const DIR_TARGET = "asset-web";
|
||||
|
||||
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
|
||||
|
||||
const headers = {
|
||||
Authorization: `Token ${TOKEN}`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Ambil list file di directory
|
||||
*/
|
||||
async function getDirItems(): Promise<DirItem[]> {
|
||||
const res = await fetch(
|
||||
`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed get dir items: ${res.statusText}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil download URL file
|
||||
*/
|
||||
async function getDownloadUrl(filePath: string): Promise<string> {
|
||||
|
||||
|
||||
const res = await fetch(
|
||||
`${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed get file url: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil semua download URL dari target dir
|
||||
*/
|
||||
async function getAllDownloadUrls() {
|
||||
const items = await getDirItems();
|
||||
|
||||
const files = items.filter((item) => item.type === "file");
|
||||
|
||||
const results = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filePath = `${DIR_TARGET}/${file.name}`;
|
||||
const url = await getDownloadUrl(filePath);
|
||||
return {
|
||||
name: file.name,
|
||||
path: filePath,
|
||||
downloadUrl: url,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// contoh eksekusi
|
||||
(async () => {
|
||||
try {
|
||||
console.log("ambil gambar")
|
||||
const urls = await getAllDownloadUrls();
|
||||
await Bun.write("list_image2.json", JSON.stringify(urls))
|
||||
console.log("selesai !")
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
serverExternalPackages: ['@elysiajs/static', 'elysia'],
|
||||
experimental: {},
|
||||
allowedDevOrigins: [
|
||||
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
||||
@@ -19,7 +20,6 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
37
package.json
37
package.json
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"name": "desa-darmasaba",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"test:api": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test": "bun run test:api && bun run test:e2e",
|
||||
"gen:api": ""
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "bun run prisma/seed.ts"
|
||||
@@ -30,7 +34,7 @@
|
||||
"@mantine/modals": "^8.3.6",
|
||||
"@mantine/tiptap": "^7.17.4",
|
||||
"@paljs/types": "^8.1.0",
|
||||
"@prisma/client": "^6.3.1",
|
||||
"@prisma/client": "6.3.1",
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"@tiptap/extension-highlight": "^2.11.7",
|
||||
"@tiptap/extension-link": "^2.11.7",
|
||||
@@ -45,17 +49,21 @@
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"add": "^2.0.6",
|
||||
"adm-zip": "^0.5.16",
|
||||
"animate.css": "^4.1.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"bun": "^1.2.2",
|
||||
"chart.js": "^4.4.8",
|
||||
"classnames": "^2.5.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"colors": "^1.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.3.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"elysia": "^1.3.5",
|
||||
"embla-carousel": "^8.6.0",
|
||||
@@ -63,7 +71,7 @@
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"form-data": "^4.0.2",
|
||||
"framer-motion": "^12.23.5",
|
||||
"framer-motion": "^12.38.0",
|
||||
"get-port": "^7.1.0",
|
||||
"iron-session": "^8.0.4",
|
||||
"jose": "^6.1.0",
|
||||
@@ -72,6 +80,7 @@
|
||||
"leaflet": "^1.9.4",
|
||||
"list": "^2.0.19",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^3.0.2",
|
||||
"motion": "^12.4.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"next": "^15.5.2",
|
||||
@@ -81,7 +90,7 @@
|
||||
"p-limit": "^6.2.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.6",
|
||||
"prisma": "^6.3.1",
|
||||
"prisma": "6.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-exif-orientation-img": "^0.1.5",
|
||||
@@ -92,7 +101,7 @@
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"readdirp": "^4.1.1",
|
||||
"recharts": "^2.15.3",
|
||||
"recharts": "^3.8.0",
|
||||
"sharp": "^0.34.3",
|
||||
"swr": "^2.3.2",
|
||||
"uuid": "^11.1.0",
|
||||
@@ -102,16 +111,26 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@types/cli-progress": "^3.11.6",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"eslint-config-next": "15.5.12",
|
||||
"jsdom": "^28.0.0",
|
||||
"msw": "^2.12.9",
|
||||
"parcel": "^2.6.2",
|
||||
"playwright-mcp": "^0.0.19",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e7]:
|
||||
- button "Darmasaba Logo" [ref=e8] [cursor=pointer]:
|
||||
- img "Darmasaba Logo" [ref=e10]
|
||||
- button "PPID" [ref=e11] [cursor=pointer]:
|
||||
- generic [ref=e13]: PPID
|
||||
- button "Desa" [ref=e14] [cursor=pointer]:
|
||||
- generic [ref=e16]: Desa
|
||||
- button "Kesehatan" [ref=e17] [cursor=pointer]:
|
||||
- generic [ref=e19]: Kesehatan
|
||||
- button "Keamanan" [ref=e20] [cursor=pointer]:
|
||||
- generic [ref=e22]: Keamanan
|
||||
- button "Ekonomi" [ref=e23] [cursor=pointer]:
|
||||
- generic [ref=e25]: Ekonomi
|
||||
- button "Inovasi" [ref=e26] [cursor=pointer]:
|
||||
- generic [ref=e28]: Inovasi
|
||||
- button "Lingkungan" [ref=e29] [cursor=pointer]:
|
||||
- generic [ref=e31]: Lingkungan
|
||||
- button "Pendidikan" [ref=e32] [cursor=pointer]:
|
||||
- generic [ref=e34]: Pendidikan
|
||||
- button "Musik" [ref=e35] [cursor=pointer]:
|
||||
- generic [ref=e37]: Musik
|
||||
- button [ref=e38] [cursor=pointer]:
|
||||
- img [ref=e40]
|
||||
- generic [ref=e46]:
|
||||
- generic [ref=e51]:
|
||||
- generic [ref=e52]:
|
||||
- generic [ref=e53]:
|
||||
- img "Logo Darmasaba" [ref=e55]
|
||||
- img "Logo Pudak" [ref=e57]
|
||||
- generic [ref=e63]:
|
||||
- generic [ref=e65]:
|
||||
- generic [ref=e66]:
|
||||
- img [ref=e67]
|
||||
- paragraph [ref=e71]: Jam Operasional
|
||||
- generic [ref=e72]:
|
||||
- generic [ref=e74]: Buka
|
||||
- paragraph [ref=e75]: 07:30 - 15:30
|
||||
- generic [ref=e77]:
|
||||
- generic [ref=e78]:
|
||||
- img [ref=e79]
|
||||
- paragraph [ref=e82]: Hari Ini
|
||||
- generic [ref=e83]:
|
||||
- paragraph [ref=e84]: Status Kantor
|
||||
- paragraph [ref=e85]: Sedang Beroperasi
|
||||
- paragraph [ref=e95]: Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. Semua lebih mudah dengan fitur interaktif yang kami sediakan.
|
||||
- generic [ref=e102]:
|
||||
- generic [ref=e103]: Browser Anda tidak mendukung video.
|
||||
- generic [ref=e106]:
|
||||
- heading "Penghargaan Desa" [level=2] [ref=e107]
|
||||
- paragraph [ref=e110]: Sedang memuat data penghargaan...
|
||||
- button "Lihat semua penghargaan" [ref=e111] [cursor=pointer]:
|
||||
- generic [ref=e112]:
|
||||
- paragraph [ref=e114]: Lihat Semua Penghargaan
|
||||
- img [ref=e116]
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e121]:
|
||||
- heading "Layanan" [level=1] [ref=e122]
|
||||
- paragraph [ref=e123]: Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!
|
||||
- link "Detail" [ref=e125] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/layanan
|
||||
- generic [ref=e127]: Detail
|
||||
- separator [ref=e129]
|
||||
- generic [ref=e130]:
|
||||
- generic [ref=e131]:
|
||||
- paragraph [ref=e132]: Potensi Desa
|
||||
- paragraph [ref=e133]: Jelajahi berbagai potensi dan peluang yang dimiliki desa. Fitur ini membantu warga maupun pemerintah desa dalam merencanakan dan mengembangkan program berbasis kekuatan lokal.
|
||||
- paragraph [ref=e136]: Sedang memuat potensi desa...
|
||||
- button "Lihat Semua Potensi" [ref=e139] [cursor=pointer]:
|
||||
- generic [ref=e140]:
|
||||
- generic [ref=e141]: Lihat Semua Potensi
|
||||
- img [ref=e143]
|
||||
- separator [ref=e146]
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]:
|
||||
- paragraph [ref=e150]: Desa Anti Korupsi
|
||||
- paragraph [ref=e151]: Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola secara terbuka dengan melibatkan warga dalam pengawasan anggaran, sehingga digunakan tepat sasaran dan sesuai kebutuhan masyarakat.
|
||||
- link "Selengkapnya" [ref=e153] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa-anti-korupsi/detail
|
||||
- generic [ref=e155]: Selengkapnya
|
||||
- paragraph [ref=e158]: Memuat Data...
|
||||
- generic [ref=e166]:
|
||||
- heading "SDGs Desa" [level=1] [ref=e168]
|
||||
- paragraph [ref=e169]: SDGs Desa adalah upaya desa untuk menciptakan pembangunan yang maju, inklusif, dan berkelanjutan melalui 17 tujuan mulai dari pengentasan kemiskinan, pendidikan, kesehatan, hingga pelestarian lingkungan.
|
||||
- generic [ref=e170]:
|
||||
- generic [ref=e171]:
|
||||
- img [ref=e172]
|
||||
- paragraph [ref=e175]: Data SDGs Desa belum tersedia
|
||||
- link "Jelajahi Semua Tujuan SDGs Desa" [ref=e177] [cursor=pointer]:
|
||||
- /url: /darmasaba/sdgs-desa
|
||||
- paragraph [ref=e180]: Jelajahi Semua Tujuan SDGs Desa
|
||||
- generic [ref=e181]:
|
||||
- generic [ref=e183]:
|
||||
- heading "APBDes" [level=1] [ref=e184]
|
||||
- paragraph [ref=e185]: Transparansi APBDes Darmasaba adalah langkah nyata menuju tata kelola desa yang bersih, terbuka, dan bertanggung jawab.
|
||||
- link "Lihat Semua Data" [ref=e187] [cursor=pointer]:
|
||||
- /url: /darmasaba/apbdes
|
||||
- generic [ref=e189]: Lihat Semua Data
|
||||
- generic [ref=e191]:
|
||||
- paragraph [ref=e193]: Pilih Tahun APBDes
|
||||
- generic [ref=e194]:
|
||||
- textbox "Pilih Tahun APBDes" [ref=e195]:
|
||||
- /placeholder: Pilih tahun
|
||||
- generic:
|
||||
- img
|
||||
- paragraph [ref=e197]: Tidak ada data APBDes untuk tahun yang dipilih.
|
||||
- generic [ref=e202]:
|
||||
- heading "Prestasi Desa" [level=1] [ref=e203]
|
||||
- paragraph [ref=e204]: Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama.
|
||||
- link "Lihat Semua Prestasi" [ref=e205] [cursor=pointer]:
|
||||
- /url: /darmasaba/prestasi-desa
|
||||
- generic [ref=e207]: Lihat Semua Prestasi
|
||||
- button [ref=e211] [cursor=pointer]:
|
||||
- img [ref=e214]
|
||||
- button [ref=e219] [cursor=pointer]:
|
||||
- img [ref=e221]
|
||||
- generic [ref=e225]:
|
||||
- contentinfo [ref=e228]:
|
||||
- generic [ref=e230]:
|
||||
- generic [ref=e231]:
|
||||
- heading "Komitmen Layanan Kami" [level=2] [ref=e232]
|
||||
- generic [ref=e233]:
|
||||
- generic [ref=e234]:
|
||||
- paragraph [ref=e235]: "1. Transparansi:"
|
||||
- paragraph [ref=e236]: Pengelolaan dana desa dilakukan secara terbuka agar masyarakat dapat memahami dan memantau penggunaan anggaran.
|
||||
- generic [ref=e237]:
|
||||
- paragraph [ref=e238]: "2. Profesionalisme:"
|
||||
- paragraph [ref=e239]: Layanan desa diberikan secara cepat, adil, dan profesional demi kepuasan masyarakat.
|
||||
- generic [ref=e240]:
|
||||
- paragraph [ref=e241]: "3. Partisipasi:"
|
||||
- paragraph [ref=e242]: Masyarakat dilibatkan aktif dalam pengambilan keputusan demi pembangunan desa yang berhasil.
|
||||
- generic [ref=e243]:
|
||||
- paragraph [ref=e244]: "4. Inovasi:"
|
||||
- paragraph [ref=e245]: Kami terus berinovasi, termasuk melalui teknologi, agar layanan semakin mudah diakses.
|
||||
- generic [ref=e246]:
|
||||
- paragraph [ref=e247]: "5. Keadilan:"
|
||||
- paragraph [ref=e248]: Kebijakan dan program disusun untuk memberi manfaat yang merata bagi seluruh warga.
|
||||
- generic [ref=e249]:
|
||||
- paragraph [ref=e250]: "6. Pemberdayaan:"
|
||||
- paragraph [ref=e251]: Masyarakat didukung melalui pelatihan, pendampingan, dan pengembangan usaha lokal.
|
||||
- generic [ref=e252]:
|
||||
- paragraph [ref=e253]: "7. Ramah Lingkungan:"
|
||||
- paragraph [ref=e254]: Seluruh kegiatan pembangunan memperhatikan keberlanjutan demi menjaga alam dan kesehatan warga.
|
||||
- separator [ref=e255]
|
||||
- generic [ref=e256]:
|
||||
- heading "Visi Kami" [level=2] [ref=e257]
|
||||
- paragraph [ref=e258]: Dengan visi ini, kami berkomitmen menjadikan desa sebagai tempat yang aman, sejahtera, dan nyaman bagi seluruh warga.
|
||||
- paragraph [ref=e259]: Kami percaya kemajuan dimulai dari kerja sama antara pemerintah desa dan masyarakat, didukung tata kelola yang baik demi kepentingan bersama. Saran maupun keluhan dapat disampaikan melalui kontak di bawah ini.
|
||||
- generic [ref=e260]:
|
||||
- paragraph [ref=e261]: "\"Desa Kuat, Warga Sejahtera!\""
|
||||
- button "Logo Desa" [ref=e262] [cursor=pointer]:
|
||||
- generic [ref=e263]:
|
||||
- img "Logo Desa"
|
||||
- generic [ref=e265]:
|
||||
- generic [ref=e267]:
|
||||
- paragraph [ref=e268]: Tentang Darmasaba
|
||||
- paragraph [ref=e269]: Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
|
||||
- generic [ref=e270]:
|
||||
- link [ref=e271] [cursor=pointer]:
|
||||
- /url: https://www.facebook.com/DarmasabaDesaku
|
||||
- img [ref=e273]
|
||||
- link [ref=e275] [cursor=pointer]:
|
||||
- /url: https://www.instagram.com/ddarmasaba/
|
||||
- img [ref=e277]
|
||||
- link [ref=e280] [cursor=pointer]:
|
||||
- /url: https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg
|
||||
- img [ref=e282]
|
||||
- link [ref=e285] [cursor=pointer]:
|
||||
- /url: https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc
|
||||
- img [ref=e287]
|
||||
- generic [ref=e290]:
|
||||
- paragraph [ref=e291]: Layanan Desa
|
||||
- link "Administrasi Kependudukan" [ref=e292] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/layanan/
|
||||
- link "Layanan Sosial" [ref=e293] [cursor=pointer]:
|
||||
- /url: /darmasaba/ekonomi/program-kemiskinan
|
||||
- link "Pengaduan Masyarakat" [ref=e294] [cursor=pointer]:
|
||||
- /url: /darmasaba/keamanan/laporan-publik
|
||||
- link "Informasi Publik" [ref=e295] [cursor=pointer]:
|
||||
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
|
||||
- generic [ref=e297]:
|
||||
- paragraph [ref=e298]: Tautan Penting
|
||||
- link "Portal Badung" [ref=e299] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/berita/semua
|
||||
- link "E-Government" [ref=e300] [cursor=pointer]:
|
||||
- /url: /darmasaba/inovasi/desa-digital-smart-village
|
||||
- link "Transparansi" [ref=e301] [cursor=pointer]:
|
||||
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
|
||||
- generic [ref=e303]:
|
||||
- paragraph [ref=e304]: Berlangganan Info
|
||||
- paragraph [ref=e305]: Dapatkan kabar terbaru tentang program dan kegiatan desa langsung ke email Anda.
|
||||
- generic [ref=e306]:
|
||||
- generic [ref=e308]:
|
||||
- textbox "Masukkan email Anda" [ref=e309]
|
||||
- img [ref=e311]
|
||||
- button "Daftar" [ref=e314] [cursor=pointer]:
|
||||
- generic [ref=e316]: Daftar
|
||||
- separator [ref=e317]
|
||||
- paragraph [ref=e318]: © 2025 Desa Darmasaba. Hak cipta dilindungi.
|
||||
- region "Notifications Alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e324] [cursor=pointer]:
|
||||
- img [ref=e325]
|
||||
- alert [ref=e328]
|
||||
```
|
||||
85
playwright-report/index.html
Normal file
85
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
25
playwright.config.ts
Normal file
25
playwright.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './__tests__/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'bun run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
/* Mobile first */
|
||||
'mantine-breakpoint-xs': '30em', // 480px → mobile kecil–normal
|
||||
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
|
||||
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
|
||||
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
|
||||
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
57
prisma/_seeder_list/core/seed_app_menu.ts
Normal file
57
prisma/_seeder_list/core/seed_app_menu.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const appMenuJson = loadJsonData("core/app-menu.json");
|
||||
const appMenuChildJson = loadJsonData("core/app-menu-child.json");
|
||||
|
||||
export async function seedAppMenu() {
|
||||
console.log("🔄 Seeding AppMenu...");
|
||||
|
||||
for (const item of appMenuJson) {
|
||||
await prisma.appMenu.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ AppMenu seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 AppMenu seed selesai");
|
||||
}
|
||||
|
||||
export async function seedAppMenuChild() {
|
||||
console.log("🔄 Seeding AppMenuChild...");
|
||||
|
||||
for (const item of appMenuChildJson) {
|
||||
await prisma.appMenuChild.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
appMenuId: item.appMenuId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
appMenuId: item.appMenuId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ AppMenuChild seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 AppMenuChild seed selesai");
|
||||
}
|
||||
69
prisma/_seeder_list/core/seed_core.ts
Normal file
69
prisma/_seeder_list/core/seed_core.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const layananJson = loadJsonData("core/layanan.json");
|
||||
const potensiJson = loadJsonData("core/potensi.json");
|
||||
const landingPageLayananJson = loadJsonData("core/landingpage-layanan.json");
|
||||
|
||||
export async function seedLayananCore() {
|
||||
console.log("🔄 Seeding Layanan...");
|
||||
|
||||
for (const item of layananJson) {
|
||||
await prisma.layanan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Layanan seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Layanan seed selesai");
|
||||
}
|
||||
|
||||
export async function seedPotensiCore() {
|
||||
console.log("🔄 Seeding Potensi...");
|
||||
|
||||
for (const item of potensiJson) {
|
||||
await prisma.potensi.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Potensi seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Potensi seed selesai");
|
||||
}
|
||||
|
||||
export async function seedLandingPageLayanan() {
|
||||
console.log("🔄 Seeding LandingPage_Layanan...");
|
||||
|
||||
for (const item of landingPageLayananJson) {
|
||||
await prisma.landingPage_Layanan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
deksripsi: item.deksripsi,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
deksripsi: item.deksripsi,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ LandingPage_Layanan seeded: ${item.id}`);
|
||||
}
|
||||
|
||||
console.log("🎉 LandingPage_Layanan seed selesai");
|
||||
}
|
||||
96
prisma/_seeder_list/desa/berita/seed_berita.ts
Normal file
96
prisma/_seeder_list/desa/berita/seed_berita.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const kategoriBerita = loadJsonData("desa/berita/kategori-berita.json");
|
||||
const beritaJson = loadJsonData("desa/berita/berita.json");
|
||||
|
||||
export async function seedBerita() {
|
||||
// ================== SUBMENU BERITA ========================
|
||||
console.log("🔄 Seeding Kategori Berita...");
|
||||
for (const k of kategoriBerita) {
|
||||
await prisma.kategoriBerita.upsert({
|
||||
where: {
|
||||
name: k.name, // ✅ cocok dengan @unique
|
||||
},
|
||||
update: {
|
||||
name: k.name,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
id: k.id, // ✅ id tetap bisa disimpan
|
||||
name: k.name,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("kategori berita success ...");
|
||||
|
||||
console.log("🔄 Seeding Berita...");
|
||||
|
||||
// Build a map of valid kategori IDs
|
||||
const validKategoriIds = new Set<string>();
|
||||
const kategoriList = await prisma.kategoriBerita.findMany({
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
kategoriList.forEach((k) => validKategoriIds.add(k.id));
|
||||
|
||||
console.log(`📋 Found ${validKategoriIds.size} valid kategori IDs in database`);
|
||||
|
||||
for (const b of beritaJson) {
|
||||
// Validate kategoriBeritaId exists
|
||||
if (!b.kategoriBeritaId || !validKategoriIds.has(b.kategoriBeritaId)) {
|
||||
console.warn(
|
||||
`⚠️ Skipping berita "${b.judul}": Invalid kategoriBeritaId "${b.kategoriBeritaId}"`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (b.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: b.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for berita "${b.judul}": ${b.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.berita.upsert({
|
||||
where: { id: b.id },
|
||||
update: {
|
||||
judul: b.judul,
|
||||
deskripsi: b.deskripsi,
|
||||
content: b.content,
|
||||
kategoriBeritaId: b.kategoriBeritaId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: b.id,
|
||||
judul: b.judul,
|
||||
deskripsi: b.deskripsi,
|
||||
content: b.content,
|
||||
kategoriBeritaId: b.kategoriBeritaId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Berita seeded: ${b.judul}`);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`❌ Failed to seed berita "${b.judul}": ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🎉 Berita seed selesai");
|
||||
}
|
||||
|
||||
42
prisma/_seeder_list/desa/gallery/foto/seed_foto.ts
Normal file
42
prisma/_seeder_list/desa/gallery/foto/seed_foto.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../../load-json";
|
||||
|
||||
const foto = loadJsonData("desa/gallery/foto/foto.json");
|
||||
|
||||
export async function seedFoto() {
|
||||
console.log("🔄 Seeding Foto...");
|
||||
for (const f of foto) {
|
||||
let imagesId: string | null = null;
|
||||
|
||||
if (f.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: f.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for foto "${f.name}": ${f.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imagesId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.galleryFoto.upsert({
|
||||
where: { id: f.id },
|
||||
update: {
|
||||
name: f.name,
|
||||
deskripsi: f.deskripsi,
|
||||
imagesId,
|
||||
},
|
||||
create: {
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
deskripsi: f.deskripsi,
|
||||
imagesId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Foto seeding completed");
|
||||
}
|
||||
27
prisma/_seeder_list/desa/gallery/video/seed_video.ts
Normal file
27
prisma/_seeder_list/desa/gallery/video/seed_video.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../../load-json";
|
||||
|
||||
const galleryVideo = loadJsonData("desa/gallery/video/video.json");
|
||||
|
||||
export async function seedVideo() {
|
||||
console.log("🔄 Seeding Gallery Video...");
|
||||
for (const v of galleryVideo) {
|
||||
await prisma.galleryVideo.upsert({
|
||||
where: {
|
||||
id: v.id,
|
||||
},
|
||||
update: {
|
||||
name: v.judul,
|
||||
deskripsi: v.deskripsi,
|
||||
linkVideo: v.linkVideo,
|
||||
},
|
||||
create: {
|
||||
name: v.judul,
|
||||
deskripsi: v.deskripsi,
|
||||
linkVideo: v.linkVideo,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("gallery video success ...");
|
||||
}
|
||||
130
prisma/_seeder_list/desa/layanan/seed_layanan.ts
Normal file
130
prisma/_seeder_list/desa/layanan/seed_layanan.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const pelayananSuratKeterangan = loadJsonData("desa/layanan/pelayananSuratKeterangan.json");
|
||||
const pelayananTelunjukSaktiDesa = loadJsonData("desa/layanan/pelayananTelunjukSaktiDesa.json");
|
||||
const pelayananPerizinanBerusaha = loadJsonData("desa/layanan/pelayananPerizinanBerusaha.json");
|
||||
const pelayananPendudukNonPermanen = loadJsonData("desa/layanan/pelayananPendudukNonPermanen.json");
|
||||
|
||||
export async function seedLayanan() {
|
||||
console.log("🔄 Seeding Pelayanan Surat Keterangan...");
|
||||
|
||||
for (const p of pelayananSuratKeterangan) {
|
||||
const existing = await prisma.pelayananSuratKeterangan.findUnique({
|
||||
where: { id: p.id },
|
||||
select: { imageId: true, image2Id: true }, // 📌 tambahkan image2Id
|
||||
});
|
||||
|
||||
// 1️⃣ Handle imageId
|
||||
let imageId = existing?.imageId ?? null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pelayanan surat keterangan 1 "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Handle image2Id
|
||||
let image2Id = existing?.image2Id ?? null;
|
||||
|
||||
if (p.image2Name) {
|
||||
const image2 = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.image2Name },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image2) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pelayanan surat keterangan 2 "${p.name}": ${p.image2Name}`,
|
||||
);
|
||||
} else {
|
||||
image2Id = image2.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ Upsert dengan kedua image
|
||||
await prisma.pelayananSuratKeterangan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
image2Id, // 📌 tambahkan ini
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
image2Id, // 📌 tambahkan ini
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pelayanan Surat Keterangan success...");
|
||||
|
||||
for (const p of pelayananTelunjukSaktiDesa) {
|
||||
await prisma.pelayananTelunjukSaktiDesa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
link: p.link,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
link: p.link,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pelayanan telunjuk sakti desa success ...");
|
||||
|
||||
for (const l of pelayananPerizinanBerusaha) {
|
||||
await prisma.pelayananPerizinanBerusaha.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
link: l.link,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
link: l.link,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("pelayanan perizinan berusaha success ...");
|
||||
|
||||
for (const l of pelayananPendudukNonPermanen) {
|
||||
await prisma.pelayananPendudukNonPermanen.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pelayanan penduduk non permanen success ...");
|
||||
}
|
||||
57
prisma/_seeder_list/desa/musik-desa/seed_musik_desa.ts
Normal file
57
prisma/_seeder_list/desa/musik-desa/seed_musik_desa.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const musikJson = loadJsonData("desa/musik-desa/musik-desa.json");
|
||||
|
||||
export async function seedMusikDesa() {
|
||||
console.log("Seeding Musik Desa...");
|
||||
|
||||
for (const item of musikJson) {
|
||||
let audioFileId: string | null = null;
|
||||
let coverImageId: string | null = null;
|
||||
|
||||
if (item.audioFileName) {
|
||||
const audio = await prisma.fileStorage.findUnique({
|
||||
where: { name: item.audioFileName },
|
||||
select: { id: true },
|
||||
});
|
||||
if (audio) audioFileId = audio.id;
|
||||
}
|
||||
|
||||
if (item.coverImageName) {
|
||||
const cover = await prisma.fileStorage.findUnique({
|
||||
where: { name: item.coverImageName },
|
||||
select: { id: true },
|
||||
});
|
||||
if (cover) coverImageId = cover.id;
|
||||
}
|
||||
|
||||
await prisma.musikDesa.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
judul: item.judul,
|
||||
artis: item.artis,
|
||||
deskripsi: item.deskripsi,
|
||||
durasi: item.durasi,
|
||||
audioFileId,
|
||||
coverImageId,
|
||||
genre: item.genre,
|
||||
tahunRilis: item.tahunRilis,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
judul: item.judul,
|
||||
artis: item.artis,
|
||||
deskripsi: item.deskripsi,
|
||||
durasi: item.durasi,
|
||||
audioFileId,
|
||||
coverImageId,
|
||||
genre: item.genre,
|
||||
tahunRilis: item.tahunRilis,
|
||||
},
|
||||
});
|
||||
console.log(` Musik: ${item.judul} - ${item.artis}`);
|
||||
}
|
||||
|
||||
console.log("Musik Desa seed selesai");
|
||||
}
|
||||
46
prisma/_seeder_list/desa/penghargaan/penghargaan.ts
Normal file
46
prisma/_seeder_list/desa/penghargaan/penghargaan.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const penghargaan = loadJsonData("desa/penghargaan/penghargaan.json");
|
||||
|
||||
export async function seedPenghargaan() {
|
||||
console.log("🔄 Seeding Penghargaan...");
|
||||
for (const m of penghargaan) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for penghargaan "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.penghargaan.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
juara: m.juara,
|
||||
deskripsi: m.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
juara: m.juara,
|
||||
deskripsi: m.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("penghargaan success ...");
|
||||
}
|
||||
|
||||
45
prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts
Normal file
45
prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
import { safeSeedUnique } from "../../../safeseedUnique";
|
||||
|
||||
const kategoriPengumuman = loadJsonData("desa/pengumuman/kategori-pengumuman.json");
|
||||
const pengumuman = loadJsonData("desa/pengumuman/pengumuman.json");
|
||||
|
||||
export async function seedPengumuman() {
|
||||
console.log("🔄 Seeding Kategori Pengumuman...");
|
||||
for (const c of kategoriPengumuman) {
|
||||
await safeSeedUnique(
|
||||
"categoryPengumuman",
|
||||
{ name: c.name }, // ✅ where clause
|
||||
{
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
console.log("kategori pengumuman success ...");
|
||||
|
||||
console.log("🔄 Seeding Pengumuman...");
|
||||
for (const p of pengumuman) {
|
||||
await prisma.pengumuman.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
content: p.content,
|
||||
categoryPengumumanId: p.categoryPengumumanId,
|
||||
},
|
||||
create: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
content: p.content,
|
||||
categoryPengumumanId: p.categoryPengumumanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("pengumuman success ...");
|
||||
}
|
||||
66
prisma/_seeder_list/desa/potensi/seed_potensi.ts
Normal file
66
prisma/_seeder_list/desa/potensi/seed_potensi.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const kategoriPotensi = loadJsonData("desa/potensi/kategori-potensi.json");
|
||||
const potensiDesa = loadJsonData("desa/potensi/potensi-desa.json");
|
||||
|
||||
export async function seedPotensi() {
|
||||
console.log("🔄Seeding Kategori Potensi Desa ...");
|
||||
for (const c of kategoriPotensi) {
|
||||
await prisma.kategoriPotensi.upsert({
|
||||
where: {
|
||||
id: c.id,
|
||||
},
|
||||
update: {
|
||||
nama: c.nama,
|
||||
},
|
||||
create: {
|
||||
id: c.id,
|
||||
nama: c.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("kategori Potensi success ...");
|
||||
|
||||
console.log("🔄 Seeding Potensi Desa...");
|
||||
for (const m of potensiDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for potensi desa "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.potensiDesa.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
deskripsi: m.deskripsi,
|
||||
content: m.content,
|
||||
kategoriId: m.kategoriId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
deskripsi: m.deskripsi,
|
||||
content: m.content,
|
||||
kategoriId: m.kategoriId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("potensi desa success ...");
|
||||
}
|
||||
170
prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts
Normal file
170
prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const lambangDesa = loadJsonData("desa/profile/lambang_desa.json");
|
||||
const maskotDesa = loadJsonData("desa/profile/maskot_desa.json");
|
||||
const profilePerbekel = loadJsonData("desa/profile/profil_perbekel.json");
|
||||
const profileDesaImage = loadJsonData("desa/profile/profileDesaImage.json");
|
||||
const sejarahDesa = loadJsonData("desa/profile/sejarah_desa.json");
|
||||
const visiMisiDesa = loadJsonData("desa/profile/visi_misi_desa.json");
|
||||
|
||||
export async function seedProfileDesa() {
|
||||
// =========== SEJARAH DESA ===========
|
||||
for (const l of sejarahDesa) {
|
||||
await prisma.sejarahDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("sejarah desa success ...");
|
||||
|
||||
// =========== VISI MISI DESA ===========
|
||||
for (const l of visiMisiDesa) {
|
||||
await prisma.visiMisiDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
visi: l.visi,
|
||||
misi: l.misi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
visi: l.visi,
|
||||
misi: l.misi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("visi misi desa success ...");
|
||||
|
||||
// =========== MASKOT DESA ===========
|
||||
for (const l of maskotDesa) {
|
||||
await prisma.maskotDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("maskot desa success ...");
|
||||
|
||||
console.log("🔄 Seeding Profile Desa Image...");
|
||||
for (const m of profileDesaImage) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for profile desa image "${m.label}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.profileDesaImage.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
label: m.label,
|
||||
maskotDesaId: m.maskotDesaId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
maskotDesaId: m.maskotDesaId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("profile desa image success ...");
|
||||
|
||||
// =========== LAMBANG DESA ===========
|
||||
for (const l of lambangDesa) {
|
||||
await prisma.lambangDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("lambang desa success ...");
|
||||
|
||||
// =========== PROFILE PERBEKEL PROFILE DESA ===========
|
||||
console.log("🔄 Seeding Profile Perbekel...");
|
||||
for (const m of profilePerbekel) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for profile perbekel "${m.biodata}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.profilPerbekel.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
biodata: m.biodata,
|
||||
pengalaman: m.pengalaman,
|
||||
pengalamanOrganisasi: m.pengalamanOrganisasi,
|
||||
programUnggulan: m.programUnggulan,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
biodata: m.biodata,
|
||||
pengalaman: m.pengalaman,
|
||||
pengalamanOrganisasi: m.pengalamanOrganisasi,
|
||||
programUnggulan: m.programUnggulan,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("profile perbekel desa success ...");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const perbekelDariMasaKeMasa = loadJsonData("desa/profile/profile-perbekel-lalu.json");
|
||||
|
||||
export async function seedProfilePerbekel() {
|
||||
console.log("🔄 Seeding Perbekel Dari Masa Ke Masa...");
|
||||
for (const p of perbekelDariMasaKeMasa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for Perbekel Dari Masa Ke Masa "${p.nama}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.perbekelDariMasaKeMasa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
nama: p.nama,
|
||||
periode: p.periode,
|
||||
daerah: p.daerah,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
nama: p.nama,
|
||||
periode: p.periode,
|
||||
daerah: p.daerah,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pejabat Desa seeding completed");
|
||||
}
|
||||
45
prisma/_seeder_list/ekonomi/seed_apbdes.ts
Normal file
45
prisma/_seeder_list/ekonomi/seed_apbdes.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const apbdesJson = loadJsonData("ekonomi/apbdes/apbdes.json");
|
||||
|
||||
export async function seedAPBDes() {
|
||||
console.log("Seeding APBDes...");
|
||||
|
||||
for (const item of apbdesJson) {
|
||||
let imageId: string | null = null;
|
||||
let fileId: string | null = null;
|
||||
|
||||
if (item.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: item.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
if (image) imageId = image.id;
|
||||
}
|
||||
|
||||
await prisma.aPBDes.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
tahun: item.tahun,
|
||||
name: item.name,
|
||||
deskripsi: item.deskripsi,
|
||||
jumlah: item.jumlah,
|
||||
imageId,
|
||||
fileId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
tahun: item.tahun,
|
||||
name: item.name,
|
||||
deskripsi: item.deskripsi,
|
||||
jumlah: item.jumlah,
|
||||
imageId,
|
||||
fileId,
|
||||
},
|
||||
});
|
||||
console.log(` APBDes: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("APBDes seed selesai");
|
||||
}
|
||||
63
prisma/_seeder_list/ekonomi/seed_apbdes_item.ts
Normal file
63
prisma/_seeder_list/ekonomi/seed_apbdes_item.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const itemsJson = loadJsonData("ekonomi/apbdes/apbdes-items.json");
|
||||
const realisasiJson = loadJsonData("ekonomi/apbdes/realisasi-items.json");
|
||||
|
||||
export async function seedAPBDesItem() {
|
||||
console.log("Seeding APBDes Items...");
|
||||
|
||||
// Seed items first (sorted by level to ensure parents exist)
|
||||
const sortedItems = [...itemsJson].sort((a, b) => a.level - b.level);
|
||||
|
||||
for (const item of sortedItems) {
|
||||
await prisma.aPBDesItem.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
kode: item.kode,
|
||||
uraian: item.uraian,
|
||||
anggaran: item.anggaran,
|
||||
tipe: item.tipe,
|
||||
level: item.level,
|
||||
parentId: item.parentId,
|
||||
apbdesId: item.apbdesId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
kode: item.kode,
|
||||
uraian: item.uraian,
|
||||
anggaran: item.anggaran,
|
||||
tipe: item.tipe,
|
||||
level: item.level,
|
||||
parentId: item.parentId,
|
||||
apbdesId: item.apbdesId,
|
||||
},
|
||||
});
|
||||
console.log(` APBDes Item: ${item.kode} - ${item.uraian}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Realisasi Items...");
|
||||
for (const item of realisasiJson) {
|
||||
await prisma.realisasiItem.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
kode: item.kode,
|
||||
apbdesItemId: item.apbdesItemId,
|
||||
jumlah: item.jumlah,
|
||||
tanggal: new Date(item.tanggal),
|
||||
keterangan: item.keterangan,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
kode: item.kode,
|
||||
apbdesItemId: item.apbdesItemId,
|
||||
jumlah: item.jumlah,
|
||||
tanggal: new Date(item.tanggal),
|
||||
keterangan: item.keterangan,
|
||||
},
|
||||
});
|
||||
console.log(` Realisasi: ${item.kode} - Rp ${item.jumlah.toLocaleString("id-ID")}`);
|
||||
}
|
||||
|
||||
console.log("APBDes Item & Realisasi seed selesai");
|
||||
}
|
||||
27
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
27
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const demografiPekerjaan = loadJsonData("ekonomi/demografi-pekerjaan/demografi-pekerjaan.json");
|
||||
|
||||
export async function seedDemografiPekerjaan() {
|
||||
console.log("🔄 Seeding Demografi Pekerjaan...");
|
||||
for (const k of demografiPekerjaan) {
|
||||
await prisma.dataDemografiPekerjaan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
pekerjaan: k.pekerjaan,
|
||||
lakiLaki: k.lakiLaki,
|
||||
perempuan: k.perempuan,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
pekerjaan: k.pekerjaan,
|
||||
lakiLaki: k.lakiLaki,
|
||||
perempuan: k.perempuan,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Demografi Pekerjaan seeded successfully");
|
||||
}
|
||||
25
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
25
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const jumlahPendudukMiskin = loadJsonData("ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json");
|
||||
|
||||
export async function seedJumlahPendudukMiskin() {
|
||||
console.log("🔄 Seeding Jumlah Penduduk Miskin...");
|
||||
for (const k of jumlahPendudukMiskin) {
|
||||
await prisma.grafikJumlahPendudukMiskin.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
year: k.year,
|
||||
totalPoorPopulation: k.totalPoorPopulation,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
year: k.year,
|
||||
totalPoorPopulation: k.totalPoorPopulation,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jumlah Penduduk Miskin seeded successfully");
|
||||
}
|
||||
29
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
29
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const jumlahPengangguran = loadJsonData("ekonomi/jumlah-pengangguran/detail-data-pengangguran.json");
|
||||
|
||||
export async function seedJumlahPengangguran() {
|
||||
for (const d of jumlahPengangguran) {
|
||||
await prisma.detailDataPengangguran.upsert({
|
||||
where: {
|
||||
month_year: { month: d.month, year: d.year },
|
||||
},
|
||||
update: {
|
||||
totalUnemployment: d.totalUnemployment,
|
||||
educatedUnemployment: d.educatedUnemployment,
|
||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||
percentageChange: d.percentageChange,
|
||||
},
|
||||
create: {
|
||||
month: d.month,
|
||||
year: d.year,
|
||||
totalUnemployment: d.totalUnemployment,
|
||||
educatedUnemployment: d.educatedUnemployment,
|
||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||
percentageChange: d.percentageChange,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 detailDataPengangguran success ...");
|
||||
}
|
||||
37
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
37
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const lowonganKerjaLokal = loadJsonData("ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json");
|
||||
|
||||
export async function seedLowonganKerjaLokal() {
|
||||
console.log("🔄 Seeding Lowongan Kerja Lokal...");
|
||||
for (const k of lowonganKerjaLokal) {
|
||||
await prisma.lowonganPekerjaan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
posisi: k.posisi,
|
||||
namaPerusahaan: k.namaPerusahaan,
|
||||
lokasi: k.lokasi,
|
||||
tipePekerjaan: k.tipePekerjaan,
|
||||
gaji: k.gaji,
|
||||
deskripsi: k.deskripsi,
|
||||
kualifikasi: k.kualifikasi,
|
||||
notelp: k.notelp,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
posisi: k.posisi,
|
||||
namaPerusahaan: k.namaPerusahaan,
|
||||
lokasi: k.lokasi,
|
||||
tipePekerjaan: k.tipePekerjaan,
|
||||
gaji: k.gaji,
|
||||
deskripsi: k.deskripsi,
|
||||
kualifikasi: k.kualifikasi,
|
||||
notelp: k.notelp,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Lowongan Kerja Lokal seeded successfully");
|
||||
}
|
||||
93
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
93
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kategoriProduk = loadJsonData("ekonomi/pasar-desa/kategori-produk.json");
|
||||
const pasarDesa = loadJsonData("ekonomi/pasar-desa/pasar-desa.json");
|
||||
const kategoriToPasar = loadJsonData("ekonomi/pasar-desa/kategori-to-pasar.json");
|
||||
|
||||
export async function seedPasarDesa() {
|
||||
console.log("🔄 Seeding Kategori Produk...");
|
||||
for (const k of kategoriProduk) {
|
||||
await prisma.kategoriProduk.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kategori Produk seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pasar Desa...");
|
||||
|
||||
for (const p of pasarDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pasar desa "${p.nama}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.pasarDesa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
nama: p.nama,
|
||||
deskripsi: p.deskripsi,
|
||||
harga: p.harga,
|
||||
rating: p.rating,
|
||||
alamatUsaha: p.alamatUsaha,
|
||||
kontak: p.kontak,
|
||||
imageId,
|
||||
kategoriProdukId: p.kategoriProdukId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
nama: p.nama,
|
||||
deskripsi: p.deskripsi,
|
||||
harga: p.harga,
|
||||
rating: p.rating,
|
||||
alamatUsaha: p.alamatUsaha,
|
||||
kontak: p.kontak,
|
||||
imageId,
|
||||
kategoriProdukId: p.kategoriProdukId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Pasar desa seeded: ${p.nama}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Pasar desa seed selesai");
|
||||
|
||||
console.log("🔄 Seeding Kategori To Pasar...");
|
||||
for (const p of kategoriToPasar) {
|
||||
await prisma.kategoriToPasar.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
kategoriId: p.kategoriId,
|
||||
pasarDesaId: p.pasarDesaId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
kategoriId: p.kategoriId,
|
||||
pasarDesaId: p.pasarDesaId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
83
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
83
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const apbdes = loadJsonData("ekonomi/pendapatan-asli-desa/apbDesa.json");
|
||||
const pendapatan = loadJsonData("ekonomi/pendapatan-asli-desa/pendapatanDesa.json");
|
||||
const belanja = loadJsonData("ekonomi/pendapatan-asli-desa/belanjaDesa.json");
|
||||
const pembiayaan = loadJsonData("ekonomi/pendapatan-asli-desa/pembiayaanDesa.json");
|
||||
|
||||
export async function seedPendapatanAsli() {
|
||||
console.log("🔄 Seeding Pendapatan Asli...");
|
||||
for (const d of apbdes) {
|
||||
await prisma.apbDesa.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
tahun: d.tahun,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
tahun: d.tahun,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pendapatan Asli seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pendapatan...");
|
||||
for (const d of pendapatan) {
|
||||
await prisma.pendapatan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pendapatan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Belanja...");
|
||||
for (const d of belanja) {
|
||||
await prisma.belanja.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Belanja seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pembiayaan...");
|
||||
for (const d of pembiayaan) {
|
||||
await prisma.pembiayaan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pembiayaan seeded successfully");
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const grafikMenganggurBerdasarkanUsia = loadJsonData("ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json");
|
||||
const grafikMenganggurBerdasarkanPendidikan = loadJsonData("ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json");
|
||||
|
||||
export async function seedPendudukUsiaKerjaYangMenganggur() {
|
||||
for (const p of grafikMenganggurBerdasarkanUsia) {
|
||||
await prisma.grafikMenganggurBerdasarkanUsia.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
usia18_25: p.usia18_25,
|
||||
usia26_35: p.usia26_35,
|
||||
usia36_45: p.usia36_45,
|
||||
usia46_keatas: p.usia46_keatas,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
usia18_25: p.usia18_25,
|
||||
usia26_35: p.usia26_35,
|
||||
usia36_45: p.usia36_45,
|
||||
usia46_keatas: p.usia46_keatas,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
||||
for (const p of grafikMenganggurBerdasarkanPendidikan) {
|
||||
await prisma.grafikMenganggurBerdasarkanPendidikan.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
SD: p.SD,
|
||||
SMP: p.SMP,
|
||||
SMA: p.SMA,
|
||||
D3: p.D3,
|
||||
S1: p.S1,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
SD: p.SD,
|
||||
SMP: p.SMP,
|
||||
SMA: p.SMA,
|
||||
D3: p.D3,
|
||||
S1: p.S1,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
||||
}
|
||||
52
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
52
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const programKemiskinan = loadJsonData("ekonomi/program-kemiskinan/program-kemiskinan.json");
|
||||
const statistikKemiskinan = loadJsonData("ekonomi/program-kemiskinan/statistik-kemiskinan.json");
|
||||
|
||||
export async function seedProgramKemiskinan() {
|
||||
for (const s of statistikKemiskinan) {
|
||||
await prisma.statistikKemiskinan.upsert({
|
||||
where: { tahun: s.tahun }, // ✅ FIX
|
||||
update: {
|
||||
jumlah: s.jumlah,
|
||||
},
|
||||
create: {
|
||||
id: s.id, // id boleh tetap
|
||||
tahun: s.tahun,
|
||||
jumlah: s.jumlah,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("📊 Statistik Kemiskinan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Program Kemiskinan...");
|
||||
for (const k of programKemiskinan) {
|
||||
await prisma.programKemiskinan.upsert({
|
||||
where: { id: k.id },
|
||||
update: {
|
||||
nama: k.nama,
|
||||
deskripsi: k.deskripsi,
|
||||
icon: k.icon,
|
||||
statistik: {
|
||||
connect: {
|
||||
tahun: k.tahun, // 👈 BUKAN ID
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
deskripsi: k.deskripsi,
|
||||
icon: k.icon,
|
||||
statistik: {
|
||||
connect: {
|
||||
tahun: k.tahun,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Program Kemiskinan seeded successfully");
|
||||
}
|
||||
27
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
27
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const sektorUnggulanDesa = loadJsonData("ekonomi/sektor-unggulan/sektor-unggulan.json");
|
||||
|
||||
export async function seedSektorUnggulanDesa() {
|
||||
console.log("🔄 Seeding Sektor Unggulan Desa...");
|
||||
for (const k of sektorUnggulanDesa) {
|
||||
await prisma.sektorUnggulanDesa.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
name: k.name,
|
||||
description: k.description,
|
||||
value: k.value,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
description: k.description,
|
||||
value: k.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Sektor Unggulan Desa seeded successfully");
|
||||
}
|
||||
80
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
80
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
interface PosisiOrganisasi {
|
||||
id: string;
|
||||
nama: string;
|
||||
deskripsi: string;
|
||||
hierarki: number;
|
||||
parentId: string | null;
|
||||
}
|
||||
|
||||
interface PegawaiBumDes {
|
||||
id: string;
|
||||
namaLengkap: string;
|
||||
gelarAkademik: string;
|
||||
tanggalMasuk: string;
|
||||
email: string;
|
||||
telepon: string;
|
||||
alamat: string;
|
||||
posisiId: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const posisiOrganisasiBumDes = loadJsonData<PosisiOrganisasi[][]>("ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json");
|
||||
const pegawai = loadJsonData<PegawaiBumDes[]>("ekonomi/struktur-organisasi/pegawai-bumdes.json");
|
||||
|
||||
export async function seedStrukturBumdes() {
|
||||
const flattenedPosisi = posisiOrganisasiBumDes.flat();
|
||||
|
||||
// ✅ Urutkan berdasarkan hierarki
|
||||
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
|
||||
|
||||
for (const p of sortedPosisi) {
|
||||
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
||||
if (p.parentId) {
|
||||
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
|
||||
if (!parentExists) {
|
||||
console.warn(
|
||||
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await prisma.posisiOrganisasiBumDes.upsert({
|
||||
where: { id: p.id },
|
||||
update: p,
|
||||
create: p,
|
||||
});
|
||||
}
|
||||
console.log("posisi organisasi berhasil");
|
||||
for (const p of pegawai) {
|
||||
await prisma.pegawaiBumDes.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
namaLengkap: p.namaLengkap,
|
||||
gelarAkademik: p.gelarAkademik,
|
||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||
email: p.email,
|
||||
telepon: p.telepon,
|
||||
alamat: p.alamat,
|
||||
posisiId: p.posisiId,
|
||||
isActive: p.isActive,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
namaLengkap: p.namaLengkap,
|
||||
gelarAkademik: p.gelarAkademik,
|
||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||
email: p.email,
|
||||
telepon: p.telepon,
|
||||
alamat: p.alamat,
|
||||
posisiId: p.posisiId,
|
||||
isActive: p.isActive,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pegawai success ...");
|
||||
}
|
||||
33
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
33
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const ajukanIde = loadJsonData("inovasi/ajukan-ide/ajukan-ide.json");
|
||||
|
||||
export async function seedAjukan() {
|
||||
console.log("🔄 Seeding Ajukan Ide Inovatif...");
|
||||
for (const d of ajukanIde) {
|
||||
await prisma.ajukanIdeInovatif.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
namaIde: d.namaIde,
|
||||
deskripsi: d.deskripsi,
|
||||
masalah: d.masalah,
|
||||
benefit: d.benefit,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
namaIde: d.namaIde,
|
||||
deskripsi: d.deskripsi,
|
||||
masalah: d.masalah,
|
||||
benefit: d.benefit,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Ajukan Ide Inovatif seeded successfully");
|
||||
}
|
||||
44
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
44
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const desaDigital = loadJsonData("inovasi/desa-digital/desa-digital.json");
|
||||
|
||||
export async function seedDesaDigital() {
|
||||
console.log("🔄 Seeding Desa Digital...");
|
||||
for (const d of desaDigital) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (d.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: d.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for desa digital "${d.name}": ${d.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.desaDigital.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
deskripsi: d.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
deskripsi: d.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Desa Digital seeded successfully");
|
||||
}
|
||||
44
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
44
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const infoTeknologi = loadJsonData("inovasi/info-teknologi/info-teknologi.json");
|
||||
|
||||
export async function seedInfoTeknologi() {
|
||||
console.log("🔄 Seeding Info Teknologi...");
|
||||
for (const p of infoTeknologi) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for berita "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.infoTekno.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Info Teknologi seeded successfully");
|
||||
}
|
||||
68
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
68
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kolaborasiInovasi = loadJsonData("inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json");
|
||||
const mitraKolaborasi = loadJsonData("inovasi/kolaborasi-inovasi/mitra-kolaborasi.json");
|
||||
|
||||
export async function seedKolaborasiInovasi() {
|
||||
console.log("🔄 Seeding Kolaborasi Inovasi...");
|
||||
for (const p of kolaborasiInovasi) {
|
||||
await prisma.kolaborasiInovasi.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
tahun: p.tahun,
|
||||
slug: p.slug,
|
||||
deskripsi: p.deskripsi,
|
||||
kolaborator: p.kolaborator,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
tahun: p.tahun,
|
||||
slug: p.slug,
|
||||
deskripsi: p.deskripsi,
|
||||
kolaborator: p.kolaborator,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kolaborasi Inovasi seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Mitra Kolaborasi...");
|
||||
for (const p of mitraKolaborasi) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for mitra kolaborasi "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.mitraKolaborasi.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Mitra Kolaborasi seeded successfully");
|
||||
}
|
||||
115
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
115
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const jenisLayanan = loadJsonData("inovasi/layanan-online-desa/jenis-layanan.json");
|
||||
const administrasiOnline = loadJsonData("inovasi/layanan-online-desa/administrasi-online.json");
|
||||
const jenisPengaduan = loadJsonData("inovasi/layanan-online-desa/jenis-pengaduan.json");
|
||||
const pengaduanMasyarakat = loadJsonData("inovasi/layanan-online-desa/pengaduan-masyarakat.json");
|
||||
|
||||
export async function seedLayananOnlineDesa() {
|
||||
console.log("🔄 Seeding Jenis Layanan...");
|
||||
for (const j of jenisLayanan) {
|
||||
await prisma.jenisLayanan.upsert({
|
||||
where: {
|
||||
id: j.id,
|
||||
},
|
||||
update: {
|
||||
nama: j.nama,
|
||||
deskripsi: j.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: j.id,
|
||||
nama: j.nama,
|
||||
deskripsi: j.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jenis Layanan seeded successfully");
|
||||
console.log("🔄 Seeding Administrasi Online...");
|
||||
for (const d of administrasiOnline) {
|
||||
await prisma.administrasiOnline.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
jenisLayananId: d.jenisLayananId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
jenisLayananId: d.jenisLayananId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Administrasi Online seeded successfully");
|
||||
console.log("🔄 Seeding Jenis Pengaduan Masyarakat...");
|
||||
for (const d of jenisPengaduan) {
|
||||
await prisma.jenisPengaduan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
nama: d.nama,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
nama: d.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jenis Pengaduan Masyarakat seeded successfully");
|
||||
console.log("🔄 Seeding Pengaduan Masyarakat...");
|
||||
for (const d of pengaduanMasyarakat) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (d.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: d.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pengaduan masyarakat "${d.name}": ${d.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.pengaduanMasyarakat.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
email: d.email,
|
||||
nik: d.nik,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
judulPengaduan: d.judulPengaduan,
|
||||
lokasiKejadian: d.lokasiKejadian,
|
||||
imageId: imageId,
|
||||
deskripsiPengaduan: d.deskripsiPengaduan,
|
||||
jenisPengaduanId: d.jenisPengaduanId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
email: d.email,
|
||||
nik: d.nik,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
judulPengaduan: d.judulPengaduan,
|
||||
lokasiKejadian: d.lokasiKejadian,
|
||||
imageId: imageId,
|
||||
deskripsiPengaduan: d.deskripsiPengaduan,
|
||||
jenisPengaduanId: d.jenisPengaduanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pengaduan Masyarakat seeded successfully");
|
||||
}
|
||||
29
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
29
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const programKreatif = loadJsonData("inovasi/program-kreatif-desa/program-kreatif-desa.json");
|
||||
|
||||
export async function seedProgramKreatifDesa() {
|
||||
console.log("🔄 Seeding Program Kreatif...");
|
||||
for (const p of programKreatif) {
|
||||
await prisma.programKreatif.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
slug: p.slug,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
slug: p.slug,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Program Kreatif seeded successfully");
|
||||
}
|
||||
46
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
46
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const keamananLingkunganJson = loadJsonData("keamanan/keamanan-lingkungan/keamanan-lingkungan.json");
|
||||
|
||||
export async function seedKeamananLingkungan() {
|
||||
console.log("🔄 Seeding Keamanan Lingkungan...");
|
||||
|
||||
for (const p of keamananLingkunganJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for keamanan lingkungan "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.keamananLingkungan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Keamanan lingkungan seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Keamanan lingkungan seed selesai");
|
||||
}
|
||||
89
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
89
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kontakDaruratKeamanan = loadJsonData("keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json");
|
||||
const kontakItem = loadJsonData("keamanan/kontak-darurat-keamanan/kontakItem.json");
|
||||
const kontakDaruratToItem = loadJsonData("keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json");
|
||||
|
||||
export async function seedKontakDaruratKeamanan() {
|
||||
console.log("🔄 Seeding Kontak Item...");
|
||||
for (const e of kontakItem) {
|
||||
await prisma.kontakItem.upsert({
|
||||
where: {
|
||||
id: e.id,
|
||||
},
|
||||
update: {
|
||||
nama: e.nama,
|
||||
icon: e.icon,
|
||||
nomorTelepon: e.nomorTelepon,
|
||||
},
|
||||
create: {
|
||||
id: e.id, // ✅ WAJIB
|
||||
nama: e.nama,
|
||||
icon: e.icon,
|
||||
nomorTelepon: e.nomorTelepon,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kontak Item seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Kontak Darurat Keamanan...");
|
||||
for (const d of kontakDaruratKeamanan) {
|
||||
await prisma.kontakDaruratKeamanan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
nama: d.nama,
|
||||
icon: d.icon,
|
||||
kategoriId: d.kategoriId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
nama: d.nama,
|
||||
icon: d.icon,
|
||||
kategoriId: d.kategoriId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Kontak Darurat Keamanan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Kontak Darurat To Item...");
|
||||
for (const f of kontakDaruratToItem) {
|
||||
// ✅ Validasi foreign keys
|
||||
const kontakDaruratExists = await prisma.kontakDaruratKeamanan.findUnique({
|
||||
where: { id: f.kontakDaruratId },
|
||||
});
|
||||
|
||||
const kontakItemExists = await prisma.kontakItem.findUnique({
|
||||
where: { id: f.kontakItemId },
|
||||
});
|
||||
|
||||
if (!kontakDaruratExists) {
|
||||
console.warn(
|
||||
`⚠️ KontakDarurat ${f.kontakDaruratId} not found, skipping...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!kontakItemExists) {
|
||||
console.warn(`⚠️ KontakItem ${f.kontakItemId} not found, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.kontakDaruratToItem.upsert({
|
||||
where: { id: f.id },
|
||||
update: {
|
||||
kontakDaruratId: f.kontakDaruratId,
|
||||
kontakItemId: f.kontakItemId,
|
||||
},
|
||||
create: {
|
||||
id: f.id,
|
||||
kontakDaruratId: f.kontakDaruratId,
|
||||
kontakItemId: f.kontakItemId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kontak Darurat To Item seeded successfully");
|
||||
}
|
||||
51
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
51
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const laporanPublik = loadJsonData("keamanan/laporan-publik/laporan-publik.json");
|
||||
const penangananLaporan = loadJsonData("keamanan/laporan-publik/penanganan-laporan.json");
|
||||
|
||||
export async function seedLaporanPublik() {
|
||||
console.log("🔄 Seeding Laporan Publik...");
|
||||
for (const l of laporanPublik) {
|
||||
await prisma.laporanPublik.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
lokasi: l.lokasi,
|
||||
tanggalWaktu: l.tanggalWaktu,
|
||||
kronologi: l.kronologi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
lokasi: l.lokasi,
|
||||
tanggalWaktu: l.tanggalWaktu,
|
||||
kronologi: l.kronologi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("laporan publik success ...");
|
||||
|
||||
console.log("🔄 Seeding Penanganan Laporan...");
|
||||
for (const l of penangananLaporan) {
|
||||
await prisma.penangananLaporanPublik.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
deskripsi: l.deskripsi,
|
||||
laporanId: l.laporanId,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
deskripsi: l.deskripsi,
|
||||
laporanId: l.laporanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("penanganan laporan success ...");
|
||||
}
|
||||
30
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
30
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const pencegahanKriminalitas = loadJsonData("keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json");
|
||||
|
||||
export async function seedPencegahanKriminalitas() {
|
||||
console.log("🔄 Seeding Pencegahan Kriminalitas...");
|
||||
for (const d of pencegahanKriminalitas) {
|
||||
await prisma.pencegahanKriminalitas.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
judul: d.judul,
|
||||
deskripsi: d.deskripsi,
|
||||
deskripsiSingkat: d.deskripsiSingkat,
|
||||
linkVideo: d.linkVideo,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
judul: d.judul,
|
||||
deskripsi: d.deskripsi,
|
||||
deskripsiSingkat: d.deskripsiSingkat,
|
||||
linkVideo: d.linkVideo,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Pencegahan Kriminalitas seeded successfully");
|
||||
}
|
||||
82
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
82
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const layananPolsek = loadJsonData("keamanan/polsek-terdekat/layanan-polsek.json");
|
||||
const polsekTerdekat = loadJsonData("keamanan/polsek-terdekat/polsek-terdekat.json");
|
||||
const layananToPolsek = loadJsonData("keamanan/polsek-terdekat/layanan-to-polsek.json");
|
||||
|
||||
export async function seedPolsekTerdekat() {
|
||||
console.log("🔄 Seeding Layanan Polsek...");
|
||||
for (const k of layananPolsek) {
|
||||
await prisma.layananPolsek.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("layanan polsek success ...");
|
||||
|
||||
console.log("🔄 Seeding Polsek Terdekat...");
|
||||
for (const k of polsekTerdekat) {
|
||||
await prisma.polsekTerdekat.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
jarakKeDesa: k.jarakKeDesa,
|
||||
alamat: k.alamat,
|
||||
nomorTelepon: k.nomorTelepon,
|
||||
jamOperasional: k.jamOperasional,
|
||||
embedMapUrl: k.embedMapUrl,
|
||||
namaTempatMaps: k.namaTempatMaps,
|
||||
alamatMaps: k.alamatMaps,
|
||||
linkPetunjukArah: k.linkPetunjukArah,
|
||||
layananPolsekId: k.layananPolsekId,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
jarakKeDesa: k.jarakKeDesa,
|
||||
alamat: k.alamat,
|
||||
nomorTelepon: k.nomorTelepon,
|
||||
jamOperasional: k.jamOperasional,
|
||||
embedMapUrl: k.embedMapUrl,
|
||||
namaTempatMaps: k.namaTempatMaps,
|
||||
alamatMaps: k.alamatMaps,
|
||||
linkPetunjukArah: k.linkPetunjukArah,
|
||||
layananPolsekId: k.layananPolsekId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("polsek terdekat success ...");
|
||||
|
||||
console.log("🔄 Seeding Layanan To Polsek...");
|
||||
for (const k of layananToPolsek) {
|
||||
await prisma.layananToPolsek.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
layananId: k.layananId,
|
||||
polsekTerdekatId: k.polsekTerdekatId,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
layananId: k.layananId,
|
||||
polsekTerdekatId: k.polsekTerdekatId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("layanan to polsek success ...");
|
||||
}
|
||||
46
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
46
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const tipsKeamananJson = loadJsonData("keamanan/tips-keamanan/tips-keamanan.json");
|
||||
|
||||
export async function seedTipsKeamanan() {
|
||||
console.log("🔄 Seeding Tips Keamanan...");
|
||||
|
||||
for (const p of tipsKeamananJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for tips keamanan "${p.judul}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.menuTipsKeamanan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Tips Keamanan seeded: ${p.judul}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Tips Keamanan seed selesai");
|
||||
}
|
||||
32
prisma/_seeder_list/kependudukan/seed_data_banjar.ts
Normal file
32
prisma/_seeder_list/kependudukan/seed_data_banjar.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const dataBanjarJson = loadJsonData("kependudukan/data-banjar/data-banjar.json");
|
||||
|
||||
export async function seedDataBanjar() {
|
||||
console.log("Seeding Data Banjar...");
|
||||
|
||||
for (const item of dataBanjarJson) {
|
||||
await prisma.dataBanjar.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
penduduk: item.penduduk,
|
||||
kk: item.kk,
|
||||
miskin: item.miskin,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
penduduk: item.penduduk,
|
||||
kk: item.kk,
|
||||
miskin: item.miskin,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
});
|
||||
console.log(` Banjar: ${item.nama} (${item.penduduk} penduduk)`);
|
||||
}
|
||||
|
||||
console.log("Data Banjar seed selesai");
|
||||
}
|
||||
32
prisma/_seeder_list/kependudukan/seed_dinamika_penduduk.ts
Normal file
32
prisma/_seeder_list/kependudukan/seed_dinamika_penduduk.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const dinamikaPendudukJson = loadJsonData("kependudukan/dinamika-penduduk/dinamika-penduduk.json");
|
||||
|
||||
export async function seedDinamikaPenduduk() {
|
||||
console.log("Seeding Dinamika Penduduk...");
|
||||
|
||||
for (const item of dinamikaPendudukJson) {
|
||||
await prisma.dinamikaPenduduk.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
tahun: item.tahun,
|
||||
kelahiran: item.kelahiran,
|
||||
kematian: item.kematian,
|
||||
masuk: item.masuk,
|
||||
keluar: item.keluar,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
tahun: item.tahun,
|
||||
kelahiran: item.kelahiran,
|
||||
kematian: item.kematian,
|
||||
masuk: item.masuk,
|
||||
keluar: item.keluar,
|
||||
},
|
||||
});
|
||||
console.log(` Tahun ${item.tahun}: ${item.kelahiran} kelahiran, ${item.kematian} kematian`);
|
||||
}
|
||||
|
||||
console.log("Dinamika Penduduk seed selesai");
|
||||
}
|
||||
28
prisma/_seeder_list/kependudukan/seed_distribusi_agama.ts
Normal file
28
prisma/_seeder_list/kependudukan/seed_distribusi_agama.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const distribusiAgamaJson = loadJsonData("kependudukan/distribusi-agama/distribusi-agama.json");
|
||||
|
||||
export async function seedDistribusiAgama() {
|
||||
console.log("Seeding Distribusi Agama...");
|
||||
|
||||
for (const item of distribusiAgamaJson) {
|
||||
await prisma.distribusiAgama.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
agama: item.agama,
|
||||
jumlah: item.jumlah,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
agama: item.agama,
|
||||
jumlah: item.jumlah,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
});
|
||||
console.log(` ${item.agama}: ${item.jumlah} penganut`);
|
||||
}
|
||||
|
||||
console.log("Distribusi Agama seed selesai");
|
||||
}
|
||||
28
prisma/_seeder_list/kependudukan/seed_distribusi_umur.ts
Normal file
28
prisma/_seeder_list/kependudukan/seed_distribusi_umur.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const distribusiUmurJson = loadJsonData("kependudukan/distribusi-umur/distribusi-umur.json");
|
||||
|
||||
export async function seedDistribusiUmur() {
|
||||
console.log("Seeding Distribusi Umur...");
|
||||
|
||||
for (const item of distribusiUmurJson) {
|
||||
await prisma.distribusiUmur.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
rentangUmur: item.rentangUmur,
|
||||
jumlah: item.jumlah,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
rentangUmur: item.rentangUmur,
|
||||
jumlah: item.jumlah,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
});
|
||||
console.log(` Rentang ${item.rentangUmur}: ${item.jumlah} jiwa`);
|
||||
}
|
||||
|
||||
console.log("Distribusi Umur seed selesai");
|
||||
}
|
||||
34
prisma/_seeder_list/kependudukan/seed_migrasi_penduduk.ts
Normal file
34
prisma/_seeder_list/kependudukan/seed_migrasi_penduduk.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const migrasiPendudukJson = loadJsonData("kependudukan/migrasi-penduduk/migrasi-penduduk.json");
|
||||
|
||||
export async function seedMigrasiPenduduk() {
|
||||
console.log("Seeding MigrASI PENDUDUK...");
|
||||
|
||||
for (const item of migrasiPendudukJson) {
|
||||
await prisma.migrasiPenduduk.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
jenis: item.jenis,
|
||||
tanggal: new Date(item.tanggal),
|
||||
asal: item.asal,
|
||||
tujuan: item.tujuan,
|
||||
alasan: item.alasan,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
jenis: item.jenis,
|
||||
tanggal: new Date(item.tanggal),
|
||||
asal: item.asal,
|
||||
tujuan: item.tujuan,
|
||||
alasan: item.alasan,
|
||||
},
|
||||
});
|
||||
console.log(` ${item.nama}: ${item.jenis} (${item.alasan})`);
|
||||
}
|
||||
|
||||
console.log("Migrasi Penduduk seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const artikelJson = loadJsonData("kesehatan/artikel-kesehatan/artikel-kesehatan.json");
|
||||
const introJson = loadJsonData("kesehatan/artikel-kesehatan/introduction.json");
|
||||
const symptomJson = loadJsonData("kesehatan/artikel-kesehatan/symptom.json");
|
||||
const preventionJson = loadJsonData("kesehatan/artikel-kesehatan/prevention.json");
|
||||
const firstAidJson = loadJsonData("kesehatan/artikel-kesehatan/first-aid.json");
|
||||
const mythVsFactJson = loadJsonData("kesehatan/artikel-kesehatan/myth-vs-fact.json");
|
||||
const doctorSignJson = loadJsonData("kesehatan/artikel-kesehatan/doctor-sign.json");
|
||||
|
||||
export async function seedArtikelKesehatan() {
|
||||
console.log("Seeding Introduction...");
|
||||
for (const item of introJson) {
|
||||
await prisma.introduction.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Symptom...");
|
||||
for (const item of symptomJson) {
|
||||
await prisma.symptom.upsert({
|
||||
where: { id: item.id },
|
||||
update: { title: item.title, content: item.content },
|
||||
create: { id: item.id, title: item.title, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Prevention...");
|
||||
for (const item of preventionJson) {
|
||||
await prisma.prevention.upsert({
|
||||
where: { id: item.id },
|
||||
update: { title: item.title, content: item.content },
|
||||
create: { id: item.id, title: item.title, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding First Aid...");
|
||||
for (const item of firstAidJson) {
|
||||
await prisma.firstAid.upsert({
|
||||
where: { id: item.id },
|
||||
update: { title: item.title, content: item.content },
|
||||
create: { id: item.id, title: item.title, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Myth vs Fact...");
|
||||
for (const item of mythVsFactJson) {
|
||||
await prisma.mythVsFact.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
title: item.title,
|
||||
mitos: item.mitos,
|
||||
fakta: item.fakta,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
mitos: item.mitos,
|
||||
fakta: item.fakta,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Doctor Sign...");
|
||||
for (const item of doctorSignJson) {
|
||||
await prisma.doctorSign.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Artikel Kesehatan...");
|
||||
for (const item of artikelJson) {
|
||||
await prisma.artikelKesehatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
title: item.title,
|
||||
content: item.content,
|
||||
introductionId: item.introductionId,
|
||||
symptomId: item.symptomId,
|
||||
preventionId: item.preventionId,
|
||||
firstAidId: item.firstAidId,
|
||||
mythVsFactId: item.mythVsFactId,
|
||||
doctorSignId: item.doctorSignId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
content: item.content,
|
||||
introductionId: item.introductionId,
|
||||
symptomId: item.symptomId,
|
||||
preventionId: item.preventionId,
|
||||
firstAidId: item.firstAidId,
|
||||
mythVsFactId: item.mythVsFactId,
|
||||
doctorSignId: item.doctorSignId,
|
||||
},
|
||||
});
|
||||
console.log(` Artikel: ${item.title}`);
|
||||
}
|
||||
|
||||
console.log("Artikel Kesehatan seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const faskesJson = loadJsonData("kesehatan/fasilitas-kesehatan/fasilitas-kesehatan.json");
|
||||
const infoUmumJson = loadJsonData("kesehatan/fasilitas-kesehatan/informasi-umum.json");
|
||||
const layananUnggulanJson = loadJsonData("kesehatan/fasilitas-kesehatan/layanan-unggulan.json");
|
||||
const dokterJson = loadJsonData("kesehatan/fasilitas-kesehatan/dokter-tenaga-medis.json");
|
||||
const fasilitasPendukungJson = loadJsonData("kesehatan/fasilitas-kesehatan/fasilitas-pendukung.json");
|
||||
const prosedurJson = loadJsonData("kesehatan/fasilitas-kesehatan/prosedur-pendaftaran.json");
|
||||
const tarifJson = loadJsonData("kesehatan/fasilitas-kesehatan/tarif-layanan.json");
|
||||
|
||||
export async function seedFasilitasKesehatan() {
|
||||
console.log("Seeding Informasi Umum...");
|
||||
for (const item of infoUmumJson) {
|
||||
await prisma.informasiUmum.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
fasilitas: item.fasilitas,
|
||||
alamat: item.alamat,
|
||||
jamOperasional: item.jamOperasional,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
fasilitas: item.fasilitas,
|
||||
alamat: item.alamat,
|
||||
jamOperasional: item.jamOperasional,
|
||||
},
|
||||
});
|
||||
console.log(` Informasi Umum: ${item.fasilitas}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Layanan Unggulan...");
|
||||
for (const item of layananUnggulanJson) {
|
||||
await prisma.layananUnggulan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Fasilitas Pendukung...");
|
||||
for (const item of fasilitasPendukungJson) {
|
||||
await prisma.fasilitasPendukung.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Prosedur Pendaftaran...");
|
||||
for (const item of prosedurJson) {
|
||||
await prisma.prosedurPendaftaran.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Tarif dan Layanan...");
|
||||
for (const item of tarifJson) {
|
||||
await prisma.tarifDanLayanan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { layanan: item.layanan, tarif: item.tarif },
|
||||
create: { id: item.id, layanan: item.layanan, tarif: item.tarif },
|
||||
});
|
||||
console.log(` Tarif: ${item.layanan}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Dokter dan Tenaga Medis...");
|
||||
for (const item of dokterJson) {
|
||||
await prisma.dokterdanTenagaMedis.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
specialist: item.specialist,
|
||||
jadwal: item.jadwal,
|
||||
jadwalLibur: item.jadwalLibur,
|
||||
jamBukaOperasional: item.jamBukaOperasional,
|
||||
jamTutupOperasional: item.jamTutupOperasional,
|
||||
jamBukaLibur: item.jamBukaLibur,
|
||||
jamTutupLibur: item.jamTutupLibur,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
specialist: item.specialist,
|
||||
jadwal: item.jadwal,
|
||||
jadwalLibur: item.jadwalLibur,
|
||||
jamBukaOperasional: item.jamBukaOperasional,
|
||||
jamTutupOperasional: item.jamTutupOperasional,
|
||||
jamBukaLibur: item.jamBukaLibur,
|
||||
jamTutupLibur: item.jamTutupLibur,
|
||||
},
|
||||
});
|
||||
console.log(` Dokter: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Fasilitas Kesehatan...");
|
||||
for (const item of faskesJson) {
|
||||
await prisma.fasilitasKesehatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
informasiUmumId: item.informasiUmumId,
|
||||
layananUnggulanId: item.layananUnggulanId,
|
||||
fasilitasPendukungId: item.fasilitasPendukungId,
|
||||
prosedurPendaftaranId: item.prosedurPendaftaranId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
informasiUmumId: item.informasiUmumId,
|
||||
layananUnggulanId: item.layananUnggulanId,
|
||||
fasilitasPendukungId: item.fasilitasPendukungId,
|
||||
prosedurPendaftaranId: item.prosedurPendaftaranId,
|
||||
},
|
||||
});
|
||||
console.log(` Fasilitas Kesehatan: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("Fasilitas Kesehatan seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const infoWabahPenyakitJson = loadJsonData("kesehatan/infowabahpenyakit/infowabahpenyakit.json");
|
||||
|
||||
export async function seedInfoWabahPenyakit() {
|
||||
console.log("🔄 Seeding Info Wabah Penyakit...");
|
||||
|
||||
for (const p of infoWabahPenyakitJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for info wabah penyakit "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.infoWabahPenyakit.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsiLengkap: p.deskripsiLengkap,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsiLengkap: p.deskripsiLengkap,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Info wabah penyakit seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Info wabah penyakit seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const jadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/jadwal-kegiatan.json");
|
||||
const infoJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/informasi-jadwal.json");
|
||||
const deskJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/deskripsi-jadwal.json");
|
||||
const layananJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/layanan-jadwal.json");
|
||||
const syaratJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/syarat-ketentuan.json");
|
||||
const dokumenJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/dokumen-jadwal.json");
|
||||
const daftarJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/pendaftaran-jadwal.json");
|
||||
|
||||
export async function seedJadwalKegiatan() {
|
||||
console.log("Seeding Informasi Jadwal Kegiatan...");
|
||||
for (const item of infoJadwalJson) {
|
||||
await prisma.informasiJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
tanggal: item.tanggal,
|
||||
waktu: item.waktu,
|
||||
lokasi: item.lokasi,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
tanggal: item.tanggal,
|
||||
waktu: item.waktu,
|
||||
lokasi: item.lokasi,
|
||||
},
|
||||
});
|
||||
console.log(` Informasi: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Deskripsi Jadwal Kegiatan...");
|
||||
for (const item of deskJadwalJson) {
|
||||
await prisma.deskripsiJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { deskripsi: item.deskripsi },
|
||||
create: { id: item.id, deskripsi: item.deskripsi },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Layanan Jadwal Kegiatan...");
|
||||
for (const item of layananJadwalJson) {
|
||||
await prisma.layananJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Syarat & Ketentuan Jadwal...");
|
||||
for (const item of syaratJadwalJson) {
|
||||
await prisma.syaratKetentuanJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Dokumen Jadwal Kegiatan...");
|
||||
for (const item of dokumenJadwalJson) {
|
||||
await prisma.dokumenJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Pendaftaran Jadwal Kegiatan...");
|
||||
for (const item of daftarJadwalJson) {
|
||||
await prisma.pendaftaranJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
tanggal: item.tanggal,
|
||||
namaOrangtua: item.namaOrtu,
|
||||
nomor: item.nomor,
|
||||
alamat: item.alamat,
|
||||
catatan: item.catatan,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
tanggal: item.tanggal,
|
||||
namaOrangtua: item.namaOrtu,
|
||||
nomor: item.nomor,
|
||||
alamat: item.alamat,
|
||||
catatan: item.catatan,
|
||||
},
|
||||
});
|
||||
console.log(` Pendaftaran: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Jadwal Kegiatan...");
|
||||
for (const item of jadwalJson) {
|
||||
await prisma.jadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
content: item.content,
|
||||
informasiJadwalKegiatanId: item.informasiJadwalKegiatanId,
|
||||
deskripsiJadwalKegiatanId: item.deskripsiJadwalKegiatanId,
|
||||
layananJadwalKegiatanId: item.layananJadwalKegiatanId,
|
||||
syaratKetentuanJadwalKegiatanId: item.syaratKetentuanJadwalKegiatanId,
|
||||
dokumenJadwalKegiatanId: item.dokumenJadwalKegiatanId,
|
||||
pendaftaranJadwalKegiatanId: item.pendaftaranJadwalKegiatanId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
content: item.content,
|
||||
informasiJadwalKegiatanId: item.informasiJadwalKegiatanId,
|
||||
deskripsiJadwalKegiatanId: item.deskripsiJadwalKegiatanId,
|
||||
layananJadwalKegiatanId: item.layananJadwalKegiatanId,
|
||||
syaratKetentuanJadwalKegiatanId: item.syaratKetentuanJadwalKegiatanId,
|
||||
dokumenJadwalKegiatanId: item.dokumenJadwalKegiatanId,
|
||||
pendaftaranJadwalKegiatanId: item.pendaftaranJadwalKegiatanId,
|
||||
},
|
||||
});
|
||||
console.log(` Jadwal Kegiatan seeded`);
|
||||
}
|
||||
|
||||
console.log("Jadwal Kegiatan seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
const kontakDaruratJson = loadJsonData("kesehatan/kontak-darurat/kontak-darurat.json");
|
||||
|
||||
export async function seedKontakDarurat() {
|
||||
console.log("🔄 Seeding Kontak Darurat...");
|
||||
|
||||
for (const p of kontakDaruratJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for kontak darurat "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.kontakDarurat.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
whatsapp: p.whatsapp,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
whatsapp: p.whatsapp,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Kontak darurat seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Kontak darurat seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const penangananDaruratJson = loadJsonData("kesehatan/penanganan-darurat/penganan-darurat.json");
|
||||
|
||||
export async function seedPenangananDarurat() {
|
||||
console.log("🔄 Seeding Penanganan Darurat...");
|
||||
|
||||
for (const p of penangananDaruratJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for penanganan darurat "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.penangananDarurat.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Penanganan darurat seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Penanganan darurat seed selesai");
|
||||
}
|
||||
50
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
50
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const posyanduJson = loadJsonData("kesehatan/posyandu/posyandu.json");
|
||||
|
||||
export async function seedPosyandu() {
|
||||
console.log("🔄 Seeding Posyandu...");
|
||||
|
||||
for (const p of posyanduJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for posyandu "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.posyandu.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
nomor: p.nomor,
|
||||
deskripsi: p.deskripsi,
|
||||
jadwalPelayanan: p.jadwalPelayanan,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
nomor: p.nomor,
|
||||
deskripsi: p.deskripsi,
|
||||
jadwalPelayanan: p.jadwalPelayanan,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Posyandu seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Posyandu seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const programKesehatanJson = loadJsonData("kesehatan/program-kesehatan/program-kesehatan.json");
|
||||
|
||||
export async function seedProgramKesehatan() {
|
||||
for (const p of programKesehatanJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for program kesehatan "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.programKesehatan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Program kesehatan seeded: ${p.name}`);
|
||||
}
|
||||
}
|
||||
97
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
97
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const puskesmasJson = loadJsonData("kesehatan/puskesmas/puskesmas.json");
|
||||
const kontakPuskesmasJson = loadJsonData("kesehatan/puskesmas/kontak-puskesmas/kontak.json");
|
||||
const jamPuskesmasJson = loadJsonData("kesehatan/puskesmas/jam-puskesmas/jam.json");
|
||||
|
||||
export async function seedPuskesmas() {
|
||||
console.log("🔄 Seeding Kontak Puskesmas...");
|
||||
for (const k of kontakPuskesmasJson) {
|
||||
await prisma.kontakPuskesmas.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
kontakPuskesmas: k.kontakPuskesmas,
|
||||
email: k.email,
|
||||
facebook: k.facebook,
|
||||
kontakUGD: k.kontakUGD,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
kontakPuskesmas: k.kontakPuskesmas,
|
||||
email: k.email,
|
||||
facebook: k.facebook,
|
||||
kontakUGD: k.kontakUGD,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("kontak puskesmas success ...");
|
||||
|
||||
console.log("🔄 Seeding Jam Puskesmas...");
|
||||
for (const k of jamPuskesmasJson) {
|
||||
await prisma.jamOperasional.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
workDays: k.workDays,
|
||||
weekDays: k.weekDays,
|
||||
holiday: k.holiday,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
workDays: k.workDays,
|
||||
weekDays: k.weekDays,
|
||||
holiday: k.holiday,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("jam puskesmas success ...");
|
||||
|
||||
console.log("🔄 Seeding Puskesmas...");
|
||||
|
||||
for (const p of puskesmasJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for puskesmas "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.puskesmas.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
alamat: p.alamat,
|
||||
jamId: p.jamId,
|
||||
kontakId: p.kontakId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
alamat: p.alamat,
|
||||
jamId: p.jamId,
|
||||
kontakId: p.kontakId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Puskesmas seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Puskesmas seed selesai");
|
||||
}
|
||||
|
||||
32
prisma/_seeder_list/kesehatan/seed_grafik_kepuasan.ts
Normal file
32
prisma/_seeder_list/kesehatan/seed_grafik_kepuasan.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const grafikKepuasanJson = loadJsonData("kesehatan/grafik-kepuasan/grafik-kepuasan.json");
|
||||
|
||||
export async function seedGrafikKepuasan() {
|
||||
console.log("Seeding Grafik Kepuasan...");
|
||||
|
||||
for (const item of grafikKepuasanJson) {
|
||||
await prisma.grafikKepuasan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
penyakit: item.penyakit,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
penyakit: item.penyakit,
|
||||
},
|
||||
});
|
||||
console.log(` Grafik Kepuasan: ${item.nama}`);
|
||||
}
|
||||
|
||||
console.log("Grafik Kepuasan seed selesai");
|
||||
}
|
||||
70
prisma/_seeder_list/kesehatan/seed_kelahiran_kematian.ts
Normal file
70
prisma/_seeder_list/kesehatan/seed_kelahiran_kematian.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kelahiranJson = loadJsonData("kesehatan/kelahiran/kelahiran.json");
|
||||
const kematianJson = loadJsonData("kesehatan/kematian/kematian.json");
|
||||
const dataKematianKelahiranJson = loadJsonData("kesehatan/kematian-kelahiran/data-kematian-kelahiran.json");
|
||||
|
||||
export async function seedKelahiranKematian() {
|
||||
console.log("Seeding Kelahiran...");
|
||||
for (const item of kelahiranJson) {
|
||||
await prisma.kelahiran.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
},
|
||||
});
|
||||
console.log(` Kelahiran: ${item.nama}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Kematian...");
|
||||
for (const item of kematianJson) {
|
||||
await prisma.kematian.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
penyebab: item.penyebab,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
penyebab: item.penyebab,
|
||||
},
|
||||
});
|
||||
console.log(` Kematian: ${item.nama}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Data Kematian-Kelahiran...");
|
||||
for (const item of dataKematianKelahiranJson) {
|
||||
await prisma.dataKematian_Kelahiran.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
kematianId: item.kematianId,
|
||||
kelahiranId: item.kelahiranId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
kematianId: item.kematianId,
|
||||
kelahiranId: item.kelahiranId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Kelahiran & Kematian seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const kategoriDesaAntiKorupsi = loadJsonData("landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json");
|
||||
const desaAntiKorupsi = loadJsonData("landing-page/desa-anti-korupsi/desaantiKorpusi.json");
|
||||
|
||||
export async function seedDesaAntiKorupsi() {
|
||||
for (const k of kategoriDesaAntiKorupsi) {
|
||||
await prisma.kategoriDesaAntiKorupsi.upsert({
|
||||
where: { id: k.id },
|
||||
update: {
|
||||
name: k.name,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("kategori desa anti korupsi success ...");
|
||||
|
||||
// =========== DESA ANTI KORUPSI ===========
|
||||
for (const p of desaAntiKorupsi) {
|
||||
await prisma.desaAntiKorupsi.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
kategoriId: p.kategoriId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
kategoriId: p.kategoriId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("desa anti korupsi success ...");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user