Compare commits
266 Commits
main
...
tasks/impl
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| f6f77d9e35 | |||
| a00481152c | |||
| 00c8caade4 | |||
| 242ea86f77 | |||
| 99c2c9c6d7 | |||
| 0209f49449 | |||
| ac2fc1a705 | |||
| 9dbe172165 | |||
| cc318d4d54 | |||
| dcb8017594 | |||
| 344c6ada6d | |||
| ec3ad12531 | |||
| dad44c0537 | |||
| 11acd04419 | |||
| 867dce42f0 | |||
| 8d49213b68 | |||
| 7bb17ddf22 | |||
| 96911e3cf1 | |||
| a4069d3cba | |||
| 9950c28b9b | |||
| ffe5e6dd9f | |||
| fa0f3538d1 | |||
| dcf195f54f | |||
| 2778f53aff | |||
| c03a6b3aed | |||
| 37ac91d4f4 | |||
| 217f4a9a3b | |||
| 5d6a7437ed | |||
| 1bb9f239db | |||
| a213ff7d37 | |||
| 752a6cabee | |||
| 134ddc6154 | |||
| 28979c6b49 | |||
| b2066caa13 | |||
| 023c77d636 | |||
| 9bf3ec72cf | |||
| f359f5b1ce | |||
| 1c1e8fb190 | |||
| 54f83da3b8 | |||
| f8985c550f | |||
| e3d909e760 | |||
| 16a8df50c1 | |||
| 0018bdc251 | |||
| 83fb39a957 | |||
| 7238692dd0 | |||
| 8b50139d79 | |||
| 066180fc0e | |||
| 67f29aabef | |||
| dbf7c34228 | |||
| 036fc86fed | |||
| 2cecec733e | |||
| c64a2e5457 | |||
| 757911d7dd | |||
| 54232e4465 | |||
| 29a9a59bca | |||
| 2fb3666e57 | |||
| e30b27f7a4 | |||
| e941ed3893 | |||
| ace5aff1b6 | |||
| 716db0adca | |||
| a291bdfb51 | |||
| 0dff8f3254 | |||
| 78b8aa74cd | |||
| a0537810e8 | |||
| b3c169a2d4 | |||
| 2608a5ffdd | |||
| 6c32f3ebdb | |||
| 0feeb4de93 | |||
| 9622eb5a9a | |||
| 417a8937f5 | |||
| db8909b9ed | |||
| f66a46f645 | |||
| fb57698dc9 | |||
| d128313e71 | |||
| 7b4bb1e58e | |||
| 0befe6a3f2 | |||
| a6663bbcee |
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
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -29,7 +29,12 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env
|
# env
|
||||||
|
# env local files (keep .env.example)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# QC
|
||||||
|
QC
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
@@ -47,9 +52,6 @@ next-env.d.ts
|
|||||||
# cache
|
# cache
|
||||||
/cache
|
/cache
|
||||||
|
|
||||||
.github/
|
|
||||||
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|
||||||
|
|||||||
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.).
|
||||||
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)
|
||||||
67
Dockerfile
Normal file
67
Dockerfile
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# ==============================
|
||||||
|
# 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/next.config.* ./
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["bun", "start"]
|
||||||
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.
|
||||||
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
|
||||||
232
QWEN.md
Normal file
232
QWEN.md
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
# 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
|
||||||
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
|
||||||
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";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
serverExternalPackages: ['@elysiajs/static', 'elysia'],
|
||||||
experimental: {},
|
experimental: {},
|
||||||
allowedDevOrigins: [
|
allowedDevOrigins: [
|
||||||
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
||||||
@@ -19,7 +20,6 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
45
package.json
45
package.json
@@ -3,9 +3,13 @@
|
|||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --bun next dev",
|
"dev": "next dev",
|
||||||
"build": "bun --bun next build",
|
"build": "next build",
|
||||||
"start": "bun --bun 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": {
|
"prisma": {
|
||||||
"seed": "bun run prisma/seed.ts"
|
"seed": "bun run prisma/seed.ts"
|
||||||
@@ -19,6 +23,7 @@
|
|||||||
"@elysiajs/static": "^1.3.0",
|
"@elysiajs/static": "^1.3.0",
|
||||||
"@elysiajs/stream": "^1.1.0",
|
"@elysiajs/stream": "^1.1.0",
|
||||||
"@elysiajs/swagger": "^1.2.0",
|
"@elysiajs/swagger": "^1.2.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/carousel": "^7.16.2",
|
"@mantine/carousel": "^7.16.2",
|
||||||
"@mantine/charts": "^7.17.1",
|
"@mantine/charts": "^7.17.1",
|
||||||
"@mantine/core": "^7.17.4",
|
"@mantine/core": "^7.17.4",
|
||||||
@@ -26,9 +31,10 @@
|
|||||||
"@mantine/dropzone": "^8.1.1",
|
"@mantine/dropzone": "^8.1.1",
|
||||||
"@mantine/form": "^8.1.0",
|
"@mantine/form": "^8.1.0",
|
||||||
"@mantine/hooks": "^7.17.4",
|
"@mantine/hooks": "^7.17.4",
|
||||||
|
"@mantine/modals": "^8.3.6",
|
||||||
"@mantine/tiptap": "^7.17.4",
|
"@mantine/tiptap": "^7.17.4",
|
||||||
"@paljs/types": "^8.1.0",
|
"@paljs/types": "^8.1.0",
|
||||||
"@prisma/client": "^6.3.1",
|
"@prisma/client": "6.3.1",
|
||||||
"@tabler/icons-react": "^3.30.0",
|
"@tabler/icons-react": "^3.30.0",
|
||||||
"@tiptap/extension-highlight": "^2.11.7",
|
"@tiptap/extension-highlight": "^2.11.7",
|
||||||
"@tiptap/extension-link": "^2.11.7",
|
"@tiptap/extension-link": "^2.11.7",
|
||||||
@@ -43,23 +49,29 @@
|
|||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.2",
|
||||||
"@types/leaflet": "^1.9.20",
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
|
"@types/mime-types": "^3.0.1",
|
||||||
"@types/nodemailer": "^7.0.2",
|
"@types/nodemailer": "^7.0.2",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
|
"async-mutex": "^0.5.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"bun": "^1.2.2",
|
"bun": "^1.2.2",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
"embla-carousel-autoplay": "^8.5.2",
|
"embla-carousel": "^8.6.0",
|
||||||
"embla-carousel-react": "^7.1.0",
|
"embla-carousel-autoplay": "^8.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
"extract-zip": "^2.0.1",
|
"extract-zip": "^2.0.1",
|
||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
"framer-motion": "^12.23.5",
|
"framer-motion": "^12.38.0",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
"iron-session": "^8.0.4",
|
"iron-session": "^8.0.4",
|
||||||
"jose": "^6.1.0",
|
"jose": "^6.1.0",
|
||||||
@@ -68,6 +80,7 @@
|
|||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"list": "^2.0.19",
|
"list": "^2.0.19",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
"motion": "^12.4.1",
|
"motion": "^12.4.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "^15.5.2",
|
"next": "^15.5.2",
|
||||||
@@ -77,9 +90,10 @@
|
|||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.6",
|
"primereact": "^10.9.6",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "6.3.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-exif-orientation-img": "^0.1.5",
|
||||||
"react-international-phone": "^4.6.0",
|
"react-international-phone": "^4.6.0",
|
||||||
"react-leaflet": "^5.0.0",
|
"react-leaflet": "^5.0.0",
|
||||||
"react-simple-toasts": "^6.1.0",
|
"react-simple-toasts": "^6.1.0",
|
||||||
@@ -87,7 +101,7 @@
|
|||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"readdirp": "^4.1.1",
|
"readdirp": "^4.1.1",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^3.8.0",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"swr": "^2.3.2",
|
"swr": "^2.3.2",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
@@ -97,16 +111,25 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@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/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@vitest/ui": "^4.0.18",
|
||||||
"eslint": "^9",
|
"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",
|
"parcel": "^2.6.2",
|
||||||
|
"playwright-mcp": "^0.0.19",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'postcss-preset-mantine': {},
|
'postcss-preset-mantine': {},
|
||||||
'postcss-simple-vars': {
|
'postcss-simple-vars': {
|
||||||
variables: {
|
variables: {
|
||||||
'mantine-breakpoint-xs': '36em',
|
/* Mobile first */
|
||||||
'mantine-breakpoint-sm': '48em',
|
'mantine-breakpoint-xs': '30em', // 480px → mobile kecil–normal
|
||||||
'mantine-breakpoint-md': '62em',
|
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
|
||||||
'mantine-breakpoint-lg': '75em',
|
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
|
||||||
'mantine-breakpoint-xl': '88em',
|
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
|
||||||
},
|
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|||||||
94
prisma/_seeder_list/desa/berita/seed_berita.ts
Normal file
94
prisma/_seeder_list/desa/berita/seed_berita.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kategoriBerita from "../../../data/desa/berita/kategori-berita.json";
|
||||||
|
import beritaJson from "../../../data/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");
|
||||||
|
}
|
||||||
|
|
||||||
40
prisma/_seeder_list/desa/gallery/foto/seed_foto.ts
Normal file
40
prisma/_seeder_list/desa/gallery/foto/seed_foto.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import foto from "../../../../data/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");
|
||||||
|
}
|
||||||
25
prisma/_seeder_list/desa/gallery/video/seed_video.ts
Normal file
25
prisma/_seeder_list/desa/gallery/video/seed_video.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import galleryVideo from "../../../../data/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 ...");
|
||||||
|
}
|
||||||
128
prisma/_seeder_list/desa/layanan/seed_layanan.ts
Normal file
128
prisma/_seeder_list/desa/layanan/seed_layanan.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import pelayananSuratKeterangan from "../../../data/desa/layanan/pelayananSuratKeterangan.json";
|
||||||
|
import pelayananTelunjukSaktiDesa from "../../../data/desa/layanan/pelayananTelunjukSaktiDesa.json";
|
||||||
|
import pelayananPerizinanBerusaha from "../../../data/desa/layanan/pelayananPerizinanBerusaha.json";
|
||||||
|
import pelayananPendudukNonPermanen from "../../../data/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 ...");
|
||||||
|
}
|
||||||
44
prisma/_seeder_list/desa/penghargaan/penghargaan.ts
Normal file
44
prisma/_seeder_list/desa/penghargaan/penghargaan.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import penghargaan from "../../../data/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 ...");
|
||||||
|
}
|
||||||
|
|
||||||
43
prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts
Normal file
43
prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import { safeSeedUnique } from "../../../safeseedUnique";
|
||||||
|
import kategoriPengumuman from "../../../data/desa/pengumuman/kategori-pengumuman.json";
|
||||||
|
import pengumuman from "../../../data/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 ...");
|
||||||
|
}
|
||||||
64
prisma/_seeder_list/desa/potensi/seed_potensi.ts
Normal file
64
prisma/_seeder_list/desa/potensi/seed_potensi.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kategoriPotensi from "../../../data/desa/potensi/kategori-potensi.json";
|
||||||
|
import potensiDesa from "../../../data/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 ...");
|
||||||
|
}
|
||||||
168
prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts
Normal file
168
prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import lambangDesa from "../../../data/desa/profile/lambang_desa.json";
|
||||||
|
import maskotDesa from "../../../data/desa/profile/maskot_desa.json";
|
||||||
|
import profilePerbekel from "../../../data/desa/profile/profil_perbekel.json";
|
||||||
|
import profileDesaImage from "../../../data/desa/profile/profileDesaImage.json";
|
||||||
|
import sejarahDesa from "../../../data/desa/profile/sejarah_desa.json";
|
||||||
|
import visiMisiDesa from "../../../data/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,42 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import perbekelDariMasaKeMasa from "../../../data/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");
|
||||||
|
}
|
||||||
25
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
25
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import demografiPekerjaan from "../../data/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");
|
||||||
|
}
|
||||||
23
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
23
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jumlahPendudukMiskin from "../../data/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");
|
||||||
|
}
|
||||||
27
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
27
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jumlahPengangguran from "../../data/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 ...");
|
||||||
|
}
|
||||||
35
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
35
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import lowonganKerjaLokal from "../../data/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");
|
||||||
|
}
|
||||||
91
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
91
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kategoriProduk from "../../data/ekonomi/pasar-desa/kategori-produk.json";
|
||||||
|
import pasarDesa from "../../data/ekonomi/pasar-desa/pasar-desa.json";
|
||||||
|
import kategoriToPasar from "../../data/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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
81
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
81
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import apbdes from "../../data/ekonomi/pendapatan-asli-desa/apbDesa.json";
|
||||||
|
import pendapatan from "../../data/ekonomi/pendapatan-asli-desa/pendapatanDesa.json";
|
||||||
|
import belanja from "../../data/ekonomi/pendapatan-asli-desa/belanjaDesa.json";
|
||||||
|
import pembiayaan from "../../data/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,50 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import grafikMenganggurBerdasarkanUsia from "../../data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json";
|
||||||
|
import grafikMenganggurBerdasarkanPendidikan from "../../data/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 ...");
|
||||||
|
}
|
||||||
50
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
50
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programKemiskinan from "../../data/ekonomi/program-kemiskinan/program-kemiskinan.json";
|
||||||
|
import statistikKemiskinan from "../../data/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");
|
||||||
|
}
|
||||||
25
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
25
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import sektorUnggulanDesa from "../../data/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");
|
||||||
|
}
|
||||||
58
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
58
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import posisiOrganisasiBumDes from "../../data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
|
||||||
|
import pegawai from "../../data/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 ...");
|
||||||
|
}
|
||||||
31
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
31
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import ajukanIde from "../../data/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");
|
||||||
|
}
|
||||||
42
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
42
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import desaDigital from "../../data/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");
|
||||||
|
}
|
||||||
42
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
42
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import infoTeknologi from "../../data/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");
|
||||||
|
}
|
||||||
66
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
66
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kolaborasiInovasi from "../../data/inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json";
|
||||||
|
import mitraKolaborasi from "../../data/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");
|
||||||
|
}
|
||||||
113
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
113
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jenisLayanan from "../../data/inovasi/layanan-online-desa/jenis-layanan.json";
|
||||||
|
import administrasiOnline from "../../data/inovasi/layanan-online-desa/administrasi-online.json";
|
||||||
|
import jenisPengaduan from "../../data/inovasi/layanan-online-desa/jenis-pengaduan.json";
|
||||||
|
import pengaduanMasyarakat from "../../data/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");
|
||||||
|
}
|
||||||
27
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
27
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programKreatif from "../../data/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");
|
||||||
|
}
|
||||||
44
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
44
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import keamananLingkunganJson from "../../data/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");
|
||||||
|
}
|
||||||
87
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
87
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kontakDaruratKeamanan from "../../data/keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json";
|
||||||
|
import kontakItem from "../../data/keamanan/kontak-darurat-keamanan/kontakItem.json";
|
||||||
|
import kontakDaruratToItem from "../../data/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");
|
||||||
|
}
|
||||||
49
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
49
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import laporanPublik from "../../data/keamanan/laporan-publik/laporan-publik.json";
|
||||||
|
import penangananLaporan from "../../data/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 ...");
|
||||||
|
}
|
||||||
28
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
28
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import pencegahanKriminalitas from "../../data/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");
|
||||||
|
}
|
||||||
80
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
80
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import layananPolsek from "../../data/keamanan/polsek-terdekat/layanan-polsek.json";
|
||||||
|
import polsekTerdekat from "../../data/keamanan/polsek-terdekat/polsek-terdekat.json";
|
||||||
|
import layananToPolsek from "../../data/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 ...");
|
||||||
|
}
|
||||||
44
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
44
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tipsKeamananJson from "../../data/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");
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import infoWabahPenyakitJson from "../../../data/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,46 @@
|
|||||||
|
import kontakDaruratJson from "../../../data/kesehatan/kontak-darurat/kontak-darurat.json";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
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,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import penangananDaruratJson from "../../../data/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");
|
||||||
|
}
|
||||||
48
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
48
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import posyanduJson from "../../../data/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,42 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programKesehatanJson from "../../../data/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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
95
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import puskesmasJson from "../../../data/kesehatan/puskesmas/puskesmas.json";
|
||||||
|
import kontakPuskesmasJson from "../../../data/kesehatan/puskesmas/kontak-puskesmas/kontak.json";
|
||||||
|
import jamPuskesmasJson from "../../../data/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");
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kategoriDesaAntiKorupsi from "../../../data/landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json"
|
||||||
|
import desaAntiKorupsi from "../../../data/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 ...");
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import prestasiDesa from "../../../data/landing-page/prestasi-desa/prestasi-desa.json"
|
||||||
|
import kategoriPrestasiDesa from "../../../data/landing-page/prestasi-desa/kategori-prestasi.json"
|
||||||
|
|
||||||
|
export async function seedPrestasiDesa() {
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Kategori Prestasi Desa...");
|
||||||
|
for (const c of kategoriPrestasiDesa) {
|
||||||
|
await prisma.kategoriPrestasiDesa.upsert({
|
||||||
|
where: { id: c.id },
|
||||||
|
update: {
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: c.id,
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("kategori prestasi desa success ...");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Prestasi Desa...");
|
||||||
|
for (const m of prestasiDesa) {
|
||||||
|
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 prestasi desa "${m.name}": ${m.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.prestasiDesa.upsert({
|
||||||
|
where: { id: m.id },
|
||||||
|
update: {
|
||||||
|
name: m.name,
|
||||||
|
deskripsi: m.deskripsi,
|
||||||
|
kategoriId: m.kategoriId,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
deskripsi: m.deskripsi,
|
||||||
|
kategoriId: m.kategoriId,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("prestasi desa success ...");
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import mediaSosial from "../../../data/landing-page/profile/mediaSosial.json"
|
||||||
|
|
||||||
|
export async function seedMediaSosial() {
|
||||||
|
console.log("🔄 Seeding Media Sosial...");
|
||||||
|
for (const m of mediaSosial) {
|
||||||
|
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 berita "${m.name}": ${m.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.mediaSosial.upsert({
|
||||||
|
where: { id: m.id },
|
||||||
|
update: {
|
||||||
|
name: m.name,
|
||||||
|
iconUrl: m.iconUrl,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
iconUrl: m.iconUrl,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("media sosial success ...");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import profilePejabatDesa from "../../../data/landing-page/profile/profile.json";
|
||||||
|
|
||||||
|
export async function seedProfileLP() {
|
||||||
|
console.log("🔄 Seeding Pejabat Desa...");
|
||||||
|
for (const p of profilePejabatDesa) {
|
||||||
|
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 profile "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.pejabatDesa.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
position: p.position,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
position: p.position,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Pejabat Desa seeding completed");
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programInovasi from "../../../data/landing-page/profile/programInovasi.json";
|
||||||
|
|
||||||
|
export async function seedProgramInovasi() {
|
||||||
|
console.log("🔄 Seeding Program Inovasi...");
|
||||||
|
|
||||||
|
for (const b of programInovasi) {
|
||||||
|
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 program inovasi "${b.name}": ${b.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.programInovasi.upsert({
|
||||||
|
where: { id: b.id },
|
||||||
|
update: {
|
||||||
|
name: b.name,
|
||||||
|
description: b.description,
|
||||||
|
link: b.link,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: b.id,
|
||||||
|
name: b.name,
|
||||||
|
description: b.description,
|
||||||
|
link: b.link,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Program Inovasi seeded: ${b.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts
Normal file
41
prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import sdgsDesa from "../../../data/landing-page/sdgs-desa/sdgs-desa.json";
|
||||||
|
|
||||||
|
export async function seedSDGSDesa() {
|
||||||
|
console.log("🔄 Seeding SDGS Desa...");
|
||||||
|
for (const m of sdgsDesa) {
|
||||||
|
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 sdgs desa "${m.name}": ${m.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.sdgsDesa.upsert({
|
||||||
|
where: { id: m.id },
|
||||||
|
update: {
|
||||||
|
name: m.name,
|
||||||
|
jumlah: m.jumlah,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
jumlah: m.jumlah,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("sdgs desa success ...");
|
||||||
|
}
|
||||||
71
prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts
Normal file
71
prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kategoriGotongRoyong from "../../data/lingkungan/gotong-royong/kategori-gotong-royong.json";
|
||||||
|
import gotongRoyong from "../../data/lingkungan/gotong-royong/gotong-royong.json";
|
||||||
|
|
||||||
|
export async function seedDataGotongRoyong() {
|
||||||
|
console.log("🔄 Seeding Kategori Gotong Royong...");
|
||||||
|
|
||||||
|
for (const k of kategoriGotongRoyong) {
|
||||||
|
await prisma.kategoriKegiatan.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: k.nama,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
nama: k.nama,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Kategori Gotong Royong seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Gotong Royong...");
|
||||||
|
for (const k of gotongRoyong) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (k.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: k.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for gotong royong "${k.judul}": ${k.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.kegiatanDesa.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: k.judul,
|
||||||
|
deskripsiSingkat: k.deskripsiSingkat,
|
||||||
|
deskripsiLengkap: k.deskripsiLengkap,
|
||||||
|
tanggal: k.tanggal,
|
||||||
|
lokasi: k.lokasi,
|
||||||
|
partisipan: k.partisipan,
|
||||||
|
imageId: imageId,
|
||||||
|
kategoriKegiatanId: k.kategoriKegiatanId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
judul: k.judul,
|
||||||
|
deskripsiSingkat: k.deskripsiSingkat,
|
||||||
|
deskripsiLengkap: k.deskripsiLengkap,
|
||||||
|
tanggal: k.tanggal,
|
||||||
|
lokasi: k.lokasi,
|
||||||
|
partisipan: k.partisipan,
|
||||||
|
imageId: imageId,
|
||||||
|
kategoriKegiatanId: k.kategoriKegiatanId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Gotong Royong seeded successfully");
|
||||||
|
}
|
||||||
27
prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts
Normal file
27
prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import dataLingkunganDesa from "../../data/lingkungan/data-lingkungan-desa/data-lingkungan-desa.json";
|
||||||
|
|
||||||
|
export async function seedDataLingkunganDesa() {
|
||||||
|
console.log("🔄 Seeding Data Lingkungan Desa...");
|
||||||
|
for (const p of dataLingkunganDesa) {
|
||||||
|
await prisma.dataLingkunganDesa.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
jumlah: p.jumlah,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
jumlah: p.jumlah,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Data Lingkungan Desa seeded successfully");
|
||||||
|
}
|
||||||
63
prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts
Normal file
63
prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tujuanEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
||||||
|
import materiEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
|
||||||
|
import contohEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
|
||||||
|
|
||||||
|
export async function seedEdukasiLingkungan() {
|
||||||
|
for (const e of tujuanEdukasiLingkungan) {
|
||||||
|
await prisma.tujuanEdukasiLingkungan.upsert({
|
||||||
|
where: {
|
||||||
|
id: e.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: e.judul,
|
||||||
|
deskripsi: e.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: e.id,
|
||||||
|
judul: e.judul,
|
||||||
|
deskripsi: e.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("tujuan edukasi lingkungan success ...");
|
||||||
|
|
||||||
|
for (const m of materiEdukasiLingkungan) {
|
||||||
|
await prisma.materiEdukasiLingkungan.upsert({
|
||||||
|
where: {
|
||||||
|
id: m.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: m.judul,
|
||||||
|
deskripsi: m.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: m.id,
|
||||||
|
judul: m.judul,
|
||||||
|
deskripsi: m.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("materi edukasi lingkungan success ...");
|
||||||
|
|
||||||
|
for (const c of contohEdukasiLingkungan) {
|
||||||
|
await prisma.contohEdukasiLingkungan.upsert({
|
||||||
|
where: {
|
||||||
|
id: c.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: c.judul,
|
||||||
|
deskripsi: c.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: c.id,
|
||||||
|
judul: c.judul,
|
||||||
|
deskripsi: c.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("contoh edukasi lingkungan success ...");
|
||||||
|
}
|
||||||
63
prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts
Normal file
63
prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import filosofiTriHita from "../../data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
|
||||||
|
import bentukKonservasiBerdasarkanAdat from "../../data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
|
||||||
|
import nilaiKonservasiAdat from "../../data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
||||||
|
|
||||||
|
export async function seedKonservasiAdatBali() {
|
||||||
|
for (const f of filosofiTriHita) {
|
||||||
|
await prisma.filosofiTriHita.upsert({
|
||||||
|
where: {
|
||||||
|
id: f.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: f.judul,
|
||||||
|
deskripsi: f.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: f.id,
|
||||||
|
judul: f.judul,
|
||||||
|
deskripsi: f.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("filosofi tri hita success ...");
|
||||||
|
|
||||||
|
for (const b of bentukKonservasiBerdasarkanAdat) {
|
||||||
|
await prisma.bentukKonservasiBerdasarkanAdat.upsert({
|
||||||
|
where: {
|
||||||
|
id: b.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: b.judul,
|
||||||
|
deskripsi: b.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: b.id,
|
||||||
|
judul: b.judul,
|
||||||
|
deskripsi: b.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("bentuk konservasi berdasarkan adat success ...");
|
||||||
|
|
||||||
|
for (const n of nilaiKonservasiAdat) {
|
||||||
|
await prisma.nilaiKonservasiAdat.upsert({
|
||||||
|
where: {
|
||||||
|
id: n.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: n.judul,
|
||||||
|
deskripsi: n.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: n.id,
|
||||||
|
judul: n.judul,
|
||||||
|
deskripsi: n.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("nilai konservasi adat success ...");
|
||||||
|
}
|
||||||
51
prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts
Normal file
51
prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import pengelolaanSampah from "../../data/lingkungan/pengelolaan-sampah/pengelolaan-sampah.json";
|
||||||
|
import keteranganBankSampah from "../../data/lingkungan/pengelolaan-sampah/keterangan-bank-sampah.json";
|
||||||
|
|
||||||
|
export async function seedPengelolaanSampah() {
|
||||||
|
console.log("🔄 Seeding Pengelolaan Sampah...");
|
||||||
|
for (const p of pengelolaanSampah) {
|
||||||
|
await prisma.pengelolaanSampah.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Pengelolaan Sampah seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Keterangan Bank Sampah...");
|
||||||
|
for (const p of keteranganBankSampah) {
|
||||||
|
await prisma.keteranganBankSampahTerdekat.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
alamat: p.alamat,
|
||||||
|
namaTempatMaps: p.namaTempatMaps,
|
||||||
|
linkPetunjukArah: p.linkPetunjukArah,
|
||||||
|
lat: p.lat,
|
||||||
|
lng: p.lng,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
alamat: p.alamat,
|
||||||
|
namaTempatMaps: p.namaTempatMaps,
|
||||||
|
linkPetunjukArah: p.linkPetunjukArah,
|
||||||
|
lat: p.lat,
|
||||||
|
lng: p.lng,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Keterangan Bank Sampah seeded successfully");
|
||||||
|
}
|
||||||
27
prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts
Normal file
27
prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programPenghijauan from "../../data/lingkungan/program-penghijauan/program-penghijauan.json";
|
||||||
|
|
||||||
|
export async function seedProgramPenghijauan() {
|
||||||
|
console.log("🔄 Seeding Program Penghijauan...");
|
||||||
|
for (const p of programPenghijauan) {
|
||||||
|
await prisma.programPenghijauan.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
judul: p.judul,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
judul: p.judul,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Program Penghijauan seeded successfully");
|
||||||
|
}
|
||||||
60
prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts
Normal file
60
prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tujuanBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json";
|
||||||
|
import lokasiJadwalBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
|
||||||
|
import fasilitasBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
|
||||||
|
|
||||||
|
export async function seedBimbinganBelajar() {
|
||||||
|
for (const t of tujuanBimbinganBelajarDesa) {
|
||||||
|
await prisma.tujuanBimbinganBelajarDesa.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ tujuan bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of lokasiJadwalBimbinganBelajarDesa) {
|
||||||
|
await prisma.lokasiJadwalBimbinganBelajarDesa.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ lokasi jadwal bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of fasilitasBimbinganBelajarDesa) {
|
||||||
|
await prisma.fasilitasBimbinganBelajarDesa.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
}
|
||||||
23
prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts
Normal file
23
prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import dataPendidikan from "../../data/pendidikan/data-pendidikan/data-pendidikan.json";
|
||||||
|
|
||||||
|
export async function seedDataPendidikan() {
|
||||||
|
console.log("🔄 Seeding Data pendidikan...");
|
||||||
|
for (const k of dataPendidikan) {
|
||||||
|
await prisma.dataPendidikan.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: k.name,
|
||||||
|
jumlah: k.jumlah,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
name: k.name,
|
||||||
|
jumlah: k.jumlah,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Data pendidikan seeded successfully");
|
||||||
|
}
|
||||||
71
prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts
Normal file
71
prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import dataPerpustakaan from "../../data/pendidikan/perpustakaan-digital/perpustakaan-digital.json";
|
||||||
|
import kategoriBuku from "../../data/pendidikan/perpustakaan-digital/kategori-buku.json";
|
||||||
|
|
||||||
|
export async function seedDataPerpustakaan() {
|
||||||
|
console.log("🔄 Seeding Kategori Buku...");
|
||||||
|
for (const k of kategoriBuku) {
|
||||||
|
await prisma.kategoriBuku.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: k.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
name: k.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Kategori Buku seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Data perpustakaan...");
|
||||||
|
for (const k of dataPerpustakaan) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (k.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: k.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for perpustakaan "${k.judul}": ${k.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.dataPerpustakaan.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: k.judul,
|
||||||
|
deskripsi: k.deskripsi,
|
||||||
|
kategoriId: k.kategoriId,
|
||||||
|
imageId: imageId
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
judul: k.judul,
|
||||||
|
deskripsi: k.deskripsi,
|
||||||
|
kategoriId: k.kategoriId,
|
||||||
|
imageId: imageId
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Data perpustakaan seeded successfully");
|
||||||
|
}
|
||||||
|
if (import.meta.main) {
|
||||||
|
seedDataPerpustakaan()
|
||||||
|
.then(() => {
|
||||||
|
console.log("seed data perpustakaan success");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("gagal seed data perpustakaan", JSON.stringify(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tujuanProgram from "../../data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||||
|
import programUnggulan from "../../data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||||
|
|
||||||
|
export async function seedInfoProgramPendidikan() {
|
||||||
|
for (const t of tujuanProgram) {
|
||||||
|
await prisma.tujuanProgram.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ tujuan program seeded (editable later via UI)");
|
||||||
|
for (const t of programUnggulan) {
|
||||||
|
await prisma.programUnggulan.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ program unggulan seeded (editable later via UI)");
|
||||||
|
}
|
||||||
74
prisma/_seeder_list/pendidikan/seed_info_sekolah.ts
Normal file
74
prisma/_seeder_list/pendidikan/seed_info_sekolah.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jenjangPendidikan from "../../data/pendidikan/info-sekolah/jenjang-pendidikan.json";
|
||||||
|
import lembagaPendidikan from "../../data/pendidikan/info-sekolah/lembaga.json";
|
||||||
|
import siswa from "../../data/pendidikan/info-sekolah/siswa.json";
|
||||||
|
import pengajar from "../../data/pendidikan/info-sekolah/pengajar.json";
|
||||||
|
|
||||||
|
export async function seedInfoSekolah() {
|
||||||
|
for (const j of jenjangPendidikan) {
|
||||||
|
await prisma.jenjangPendidikan.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Jenjang Pendidikan seeded successfully");
|
||||||
|
for (const j of lembagaPendidikan) {
|
||||||
|
await prisma.lembaga.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
jenjangId: j.jenjangId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
jenjangId: j.jenjangId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Lembaga Pendidikan seeded successfully");
|
||||||
|
for (const j of siswa) {
|
||||||
|
await prisma.siswa.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
lembagaId: j.lembagaId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
lembagaId: j.lembagaId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ siswa seeded successfully");
|
||||||
|
for (const j of pengajar) {
|
||||||
|
await prisma.pengajar.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
lembagaId: j.lembagaId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
lembagaId: j.lembagaId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ pengajar seeded successfully");
|
||||||
|
}
|
||||||
60
prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts
Normal file
60
prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tujuanProgram from "../../data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
|
||||||
|
import tempatKegiatan from "../../data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
|
||||||
|
import jenisProgramYangDiselenggarakan from "../../data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
|
||||||
|
|
||||||
|
export async function seedPendidikanNonFormal() {
|
||||||
|
for (const t of tujuanProgram) {
|
||||||
|
await prisma.tujuanPendidikanNonFormal.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of tempatKegiatan) {
|
||||||
|
await prisma.tempatKegiatan.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of jenisProgramYangDiselenggarakan) {
|
||||||
|
await prisma.jenisProgramYangDiselenggarakan.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import daftarInformasiPublik from "../../../data/ppid/daftar-informasi-publik-desa-darmasaba/daftarInformasi.json"
|
||||||
|
import jenisInformasiDiminta from "../../../data/list-jenisInfromasi.json"
|
||||||
|
import caraMemperolehInformasi from "../../../data/list-caraMemperolehInformasi.json"
|
||||||
|
import caraMemperolehSalinanInformasi from "../../../data/list-caraMemperolehSalinanInformasi.json"
|
||||||
|
|
||||||
|
export async function seedDaftarInformasiPublikPpid() {
|
||||||
|
|
||||||
|
for (const v of daftarInformasiPublik) {
|
||||||
|
// Convert string date to Date object
|
||||||
|
const tanggal = new Date(v.tanggal);
|
||||||
|
|
||||||
|
await prisma.daftarInformasiPublik.upsert({
|
||||||
|
where: {
|
||||||
|
id: v.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
jenisInformasi: v.jenisInformasi,
|
||||||
|
deskripsi: v.deskripsi,
|
||||||
|
tanggal: tanggal,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: v.id,
|
||||||
|
jenisInformasi: v.jenisInformasi,
|
||||||
|
deskripsi: v.deskripsi,
|
||||||
|
tanggal: tanggal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("daftar informasi publik PPID success ...");
|
||||||
|
|
||||||
|
for (const j of jenisInformasiDiminta) {
|
||||||
|
await prisma.jenisInformasiDiminta.upsert({
|
||||||
|
where: {
|
||||||
|
name: j.name,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: j.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: j.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("jenis informasi diminta success ...");
|
||||||
|
|
||||||
|
for (const c of caraMemperolehInformasi) {
|
||||||
|
await prisma.caraMemperolehInformasi.upsert({
|
||||||
|
where: {
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("cara memperoleh informasi success ...");
|
||||||
|
|
||||||
|
for (const c of caraMemperolehSalinanInformasi) {
|
||||||
|
await prisma.caraMemperolehSalinanInformasi.upsert({
|
||||||
|
where: {
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
name: c.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("cara memperoleh salinan informasi success ...");
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import dasarHukumPPID from "../../../data/ppid/dasar-hukum-ppid/dasarhukumPPID.json"
|
||||||
|
|
||||||
|
export async function seedDasarHukumPpid() {
|
||||||
|
for (const v of dasarHukumPPID) {
|
||||||
|
await prisma.dasarHukumPPID.upsert({
|
||||||
|
where: {
|
||||||
|
id: v.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: v.judul,
|
||||||
|
content: v.content,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: v.id,
|
||||||
|
judul: v.judul,
|
||||||
|
content: v.content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("dasar hukum PPID success ...");
|
||||||
|
}
|
||||||
54
prisma/_seeder_list/ppid/ikm/seed_ikm.ts
Normal file
54
prisma/_seeder_list/ppid/ikm/seed_ikm.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jenisKelamin from "../../../data/ppid/ikm/jenis-kelamin/jenis-kelamin.json";
|
||||||
|
import pilihanRatingResponden from "../../../data/ppid/ikm/pilihan-rating-responden/rating-responden.json";
|
||||||
|
import umurResponden from "../../../data/ppid/ikm/umur-responden/umur-responden.json";
|
||||||
|
|
||||||
|
export async function seedIkmPpid() {
|
||||||
|
for (const j of jenisKelamin) {
|
||||||
|
await prisma.jenisKelaminResponden.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: j.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
name: j.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("jenis kelamin responden success ...");
|
||||||
|
|
||||||
|
for (const r of pilihanRatingResponden) {
|
||||||
|
await prisma.pilihanRatingResponden.upsert({
|
||||||
|
where: {
|
||||||
|
id: r.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: r.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("pilihan rating responden success ...");
|
||||||
|
|
||||||
|
for (const u of umurResponden) {
|
||||||
|
await prisma.umurResponden.upsert({
|
||||||
|
where: {
|
||||||
|
id: u.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: u.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: u.id,
|
||||||
|
name: u.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("umur responden success ...");
|
||||||
|
}
|
||||||
48
prisma/_seeder_list/ppid/profil-ppid/seed_profil_ppd.ts
Normal file
48
prisma/_seeder_list/ppid/profil-ppid/seed_profil_ppd.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import profilPpd from "../../../data/ppid/profile-ppid/profilePPid.json"
|
||||||
|
|
||||||
|
export async function seedProfilPpd() {
|
||||||
|
console.log("🔄 Seeding Profil PPD...");
|
||||||
|
for (const m of profilPpd) {
|
||||||
|
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 berita "${m.name}": ${m.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.profilePPID.upsert({
|
||||||
|
where: { id: m.id },
|
||||||
|
update: {
|
||||||
|
name: m.name,
|
||||||
|
biodata: m.biodata,
|
||||||
|
riwayat: m.riwayat,
|
||||||
|
pengalaman: m.pengalaman,
|
||||||
|
unggulan: m.unggulan,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: m.id,
|
||||||
|
name: m.name,
|
||||||
|
biodata: m.biodata,
|
||||||
|
riwayat: m.riwayat,
|
||||||
|
pengalaman: m.pengalaman,
|
||||||
|
unggulan: m.unggulan,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("profil ppd success ...");
|
||||||
|
}
|
||||||
|
|
||||||
82
prisma/_seeder_list/ppid/struktur-ppid/seed_struktur_ppid.ts
Normal file
82
prisma/_seeder_list/ppid/struktur-ppid/seed_struktur_ppid.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import pegawaiPpid from "../../../data/ppid/struktur-ppid/pegawai-PPID.json"
|
||||||
|
import posisiOrganisasiPPID from "../../../data/ppid/struktur-ppid/posisi-organisasi-PPID.json"
|
||||||
|
|
||||||
|
export async function seedPegawaiPpid() {
|
||||||
|
|
||||||
|
const flattenedPosisi = posisiOrganisasiPPID.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.posisiOrganisasiPPID.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: p,
|
||||||
|
create: p,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("posisi organisasi berhasil");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Struktur Ppid...");
|
||||||
|
for (const m of pegawaiPpid) {
|
||||||
|
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 pegawai ppid "${m.namaLengkap}": ${m.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.pegawaiPPID.upsert({
|
||||||
|
where: { id: m.id },
|
||||||
|
update: {
|
||||||
|
namaLengkap: m.namaLengkap,
|
||||||
|
gelarAkademik: m.gelarAkademik,
|
||||||
|
tanggalMasuk: m.tanggalMasuk,
|
||||||
|
email: m.email,
|
||||||
|
telepon: m.telepon,
|
||||||
|
alamat: m.alamat,
|
||||||
|
imageId,
|
||||||
|
posisiId: m.posisiId,
|
||||||
|
isActive: m.isActive,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: m.id,
|
||||||
|
namaLengkap: m.namaLengkap,
|
||||||
|
gelarAkademik: m.gelarAkademik,
|
||||||
|
tanggalMasuk: m.tanggalMasuk,
|
||||||
|
email: m.email,
|
||||||
|
telepon: m.telepon,
|
||||||
|
alamat: m.alamat,
|
||||||
|
imageId,
|
||||||
|
posisiId: m.posisiId,
|
||||||
|
isActive: m.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("struktur ppid success ...");
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user