Compare commits
364 Commits
nico/8-aug
...
656ffcc561
| Author | SHA1 | Date | |
|---|---|---|---|
| 656ffcc561 | |||
| 76ffa662c5 | |||
| 46423409fd | |||
| 2edf5e9b11 | |||
| af368eeee0 | |||
| e104cd8fcc | |||
| 50801e5c8a | |||
| 80186bf493 | |||
| d84edc44f5 | |||
| 8b14c6ce44 | |||
| 5e822f0b05 | |||
| 34a37dc63b | |||
| 0e6f7e1769 | |||
| feb853d06e | |||
| 3de412afe0 | |||
| 87d234e57f | |||
| fd18a22834 | |||
| 3e8b961e52 | |||
| 82d779e5e0 | |||
| a6517166cb | |||
| 483b6be677 | |||
| f8dad0dbcd | |||
| 74301fe074 | |||
| 8b19abc628 | |||
| e73798a0f2 | |||
| 1a91f3c9ad | |||
| 9b74592101 | |||
| 55f4b94082 | |||
| b403bc754c | |||
| 67b87f145e | |||
| dd09d7c90a | |||
| 59ae8ad039 | |||
| c012d5778c | |||
| af31bd8aef | |||
| 721357adcf | |||
| 39776ec355 | |||
| 50a7356618 | |||
| 4494dd98ef | |||
| 970949a68b | |||
| 8777c45a44 | |||
| 42bcba6c96 | |||
| d1d54e5c25 | |||
| 0a4b85fd82 | |||
| b751f031cd | |||
| a3940321a7 | |||
| 3cd6fcbd81 | |||
| 7d9b7b0c60 | |||
| 0806eb2308 | |||
|
|
6064ef0759 | ||
| 1c00c326c9 | |||
| 51ce823b45 | |||
| 4eba96140d | |||
| f6f0e10935 | |||
| 2108f403aa | |||
| 4dfcf20322 | |||
| c6c3eebadf | |||
|
|
6d26ace8ab | ||
|
|
0dabc204bc | ||
|
|
e8f8b51686 | ||
|
|
a4db3a149d | ||
|
|
fece983ac5 | ||
| 8b7eef5fee | |||
| 8b22d01e0d | |||
| dc13e37a02 | |||
| 2d2cbef29b | |||
| 8c8a96b830 | |||
| dc3eccacbf | |||
| ffe94992e5 | |||
|
|
f5566bca2c | ||
|
|
ba964df32c | ||
|
|
df3f382a97 | ||
| 4fb522f88f | |||
| 85332a8225 | |||
| 3fe2a5ccab | |||
| 363bfa65fb | |||
| dccf590cbf | |||
| f076b81d14 | |||
| b5ea3216e0 | |||
| 64b116588b | |||
| 63161e1a39 | |||
| 8b8c65dd1e | |||
| 159fb3cec6 | |||
| 4821934224 | |||
| ee39b88b00 | |||
| ce46d3b5f7 | |||
| 144ac37e12 | |||
| f90477ed63 | |||
| 4a7811e06f | |||
| f63aaf916d | |||
| 3803c79c95 | |||
| 2d901912ea | |||
| a791efe76c | |||
| e9f7bc2043 | |||
| 6712da9ac2 | |||
| ac11a9367c | |||
| 67e5ceb254 | |||
| 65942ac9d2 | |||
| e0436cc384 | |||
| 63682e47b6 | |||
| f4705690a9 | |||
| 239771a714 | |||
| 03451195c8 | |||
| 597af7e716 | |||
| 0a8a026b94 | |||
| a5bd91b580 | |||
|
|
7368a367f4 | ||
|
|
ed664d5b10 | ||
| ae3187804e | |||
|
|
0ba30aa5b2 | ||
|
|
790d6535e5 | ||
|
|
46ce16ae97 | ||
| 91e32f3f1c | |||
| 4d03908f23 | |||
| 0563f9664f | |||
| 961cc32057 | |||
| fe7672e09f | |||
| 341ff5779f | |||
| 69f7b4c162 | |||
| 409ad4f1a2 | |||
| 55ea3c473a | |||
| 0160fa636d | |||
| a152eaf984 | |||
| 3684e83187 | |||
| 223b85a714 | |||
| 77c54b5c8a | |||
| f1729151b3 | |||
| 8e8c133eea | |||
| 1e7acac193 | |||
| bb80b0ecc1 | |||
| 42dcbcfb22 | |||
| 22de1aa1f3 | |||
| b1d28a8322 | |||
| b86a3a85c3 | |||
| fd63bb0fd4 | |||
| f2c9a922a6 | |||
| 92b24440fe | |||
| f0558aa0d0 | |||
| 8132609ccb | |||
| 1ddc1d7eac | |||
| aa354992e7 | |||
| d43b07c2ef | |||
| 9678e6979b | |||
| b35874b120 | |||
| 1b59d6bf09 | |||
| b69df2454e | |||
| eb1ad54db6 | |||
| df198c320a | |||
| 21ec3ad1c1 | |||
| f550e29a75 | |||
| 3a115908c4 | |||
| bb7384f1e5 | |||
| 5ff791642c | |||
| df154806f7 | |||
| b803c7a90c | |||
| 25000d0b0f | |||
| fb2fe67c23 | |||
| bbd52fb6f5 | |||
| 51460558d4 | |||
| 358ff14efe | |||
| d105ceeb6b | |||
| 6c36a15290 | |||
| da585dde99 | |||
| c865aee766 | |||
| 8afbaabd91 | |||
| 273dfdfd09 | |||
| f0425cfc47 | |||
| 1d1d8e50dc | |||
| c2ad515366 | |||
| 092afe67d2 | |||
| d9ce4aac6d | |||
| 2d9170705d | |||
| 3fcfec22fb | |||
| fdf9a951a4 | |||
| 6ca1e032a6 | |||
| ca74029688 | |||
| 78c55a8a71 | |||
| 1a8fc1a670 | |||
| 17b20e0d40 | |||
| 184854d273 | |||
| 903dc74cca | |||
| 503da91ce6 | |||
| 19235f0791 | |||
| daaed8089b | |||
| 61de7d8d33 | |||
| f436aa2ef0 | |||
| 8fb85ce56c | |||
| 50bc54ceca | |||
| 1f98b6993d | |||
| f0f201c853 | |||
| 29065cb3e2 | |||
| f3a10d63d1 | |||
| bf20cd55e8 | |||
| af60bcd6fc | |||
| 7a42bec63b | |||
| dc8793e3ae | |||
| 44c421129e | |||
| c8484357cb | |||
| 342e9bbc65 | |||
| ddff427926 | |||
| 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 | |||
| d66a952d4c | |||
| ed371bd0d9 | |||
| f82c7b86e0 | |||
| b5d6585cd5 | |||
| aa98359ef7 | |||
| 0ff0d5234a | |||
| 827c1c191a | |||
| fb596f9033 | |||
| 9055b40769 | |||
| bbf13c1cf7 | |||
| 75bf0652b1 | |||
| 0b574406e2 | |||
| ccf39bc778 | |||
| 3c21f7742c | |||
| a158241c0b | |||
| 80c5dc6361 | |||
| 8ad38fc907 | |||
| d601b2fee3 | |||
| cee0957e07 | |||
| 5c66eccf23 | |||
| f7fd9be255 | |||
| 8a6d8ed8db | |||
| 63054cedf0 | |||
| c2f1ab8179 | |||
| 295d6f7d63 | |||
| dbd56a1493 | |||
| 2a26db6e17 | |||
| 33fc472472 | |||
| d8fa56d923 | |||
| cac146471a | |||
| 3e4a7a1c0a | |||
| b5c044df6e | |||
| 0fc47c28ff | |||
| 8e25c91e85 | |||
| 068d8b1077 | |||
| 9f72e94557 | |||
| 79ad39fc55 | |||
| 39e1e7b575 | |||
| 4ceea5203f | |||
| a5d841bb6b | |||
| 6a7bd386ae | |||
| a9d98895bb | |||
| 75475dc62e | |||
| b39800a475 | |||
| 797713ef49 | |||
| 8817b937b1 | |||
| 2adf60f9eb | |||
| fa9601e126 | |||
| 7ae83788b4 | |||
| 22ec8d942d | |||
| 9f9a0fb451 | |||
| b6d6583e77 | |||
| a8fd715822 | |||
| f9530c32eb | |||
| f15ef5a275 | |||
| 3a726a3334 | |||
| b21e1f0c2e | |||
| f63249327d | |||
| bb8dab05ba | |||
| 3081e426bd | |||
| 8a275c2a32 | |||
| 8469ebd2e1 | |||
| 760ba4b6d2 | |||
| 20d4c90e60 | |||
| fafbb12a08 | |||
| 01aa0da5cc | |||
| b580978f8e | |||
| 1c01397c0d | |||
| 90a6605efd | |||
| c22d865283 | |||
| 49067f0218 | |||
| d79425d529 | |||
| 4491d23bea | |||
| 1e154ced86 | |||
| bcc51aec12 | |||
| 8d15563f15 | |||
| d7a592c635 | |||
| 5e137ba658 | |||
| c99416c7f8 | |||
| 212e2db1fb | |||
| b8a45bc451 | |||
| 0777b00a7d | |||
| a035039b2c | |||
| a6832cad40 | |||
| a1d55e2b0a | |||
| c1583c21b1 | |||
| 2fe8b8ce1a | |||
| 5cbf7810bc |
47
.dockerignore
Normal file
47
.dockerignore
Normal file
@@ -0,0 +1,47 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
bun-debug.log*
|
||||
|
||||
# Docker files
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Markdown/Documentation
|
||||
README.md
|
||||
GEMINI.md
|
||||
AGENTS.md
|
||||
AUDIT_REPORT.md
|
||||
QWEN.md
|
||||
NOTE.md
|
||||
task-project-apbdes.md
|
||||
MUSIK_CREATE_ANALYSIS.md
|
||||
darkMode.md
|
||||
/test-results
|
||||
/playwright-report
|
||||
/tmp_assets
|
||||
/foldergambar
|
||||
/googleapi
|
||||
/xx
|
||||
/xx.ts
|
||||
/xx.txt
|
||||
/test.txt
|
||||
/x.json
|
||||
/x.sh
|
||||
/xcoba.ts
|
||||
/xcoba2.ts
|
||||
/gambar.ttx
|
||||
/test-berita-state.ts
|
||||
44
.env.example
Normal file
44
.env.example
Normal file
@@ -0,0 +1,44 @@
|
||||
# Database Configuration
|
||||
DATABASE_URL="postgresql://username:password@localhost:5432/desa-darmasaba?schema=public"
|
||||
|
||||
# Seafile Configuration (File Storage)
|
||||
SEAFILE_TOKEN=your_seafile_token
|
||||
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||
SEAFILE_URL=https://your-seafile-instance.com
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||
|
||||
# Upload Configuration
|
||||
WIBU_UPLOAD_DIR=uploads
|
||||
WIBU_DOWNLOAD_DIR=./download
|
||||
|
||||
# WhatsApp Server Configuration
|
||||
WA_SERVER_TOKEN=your_whatsapp_server_token
|
||||
|
||||
# Application Configuration
|
||||
# IMPORTANT: For staging/production, set this to your actual domain
|
||||
# Local development: NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||
# Staging: NEXT_PUBLIC_BASE_URL=https://desa-darmasaba-stg.wibudev.com
|
||||
# Production: NEXT_PUBLIC_BASE_URL=https://your-production-domain.com
|
||||
# Or use relative URL '/' for automatic protocol/domain detection (recommended)
|
||||
NEXT_PUBLIC_BASE_URL=/
|
||||
|
||||
# Email Configuration (for notifications/subscriptions)
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_email_app_password
|
||||
|
||||
# Session Configuration
|
||||
BASE_SESSION_KEY=your_session_key_generate_secure_random_string
|
||||
BASE_TOKEN_KEY=your_jwt_secret_key_generate_secure_random_string
|
||||
|
||||
# Telegram Bot Configuration (for notifications)
|
||||
BOT_TOKEN=your_telegram_bot_token
|
||||
CHAT_ID=your_telegram_chat_id
|
||||
|
||||
# Session Password (for iron-session)
|
||||
SESSION_PASSWORD="your_session_password_min_32_characters_long_secure"
|
||||
|
||||
# ElevenLabs API Key (for TTS features - optional)
|
||||
ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
||||
|
||||
# Environment (optional, defaults to development)
|
||||
# NODE_ENV=development
|
||||
81
.gemini/hooks/telegram-notify.ts
Executable file
81
.gemini/hooks/telegram-notify.ts
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bun
|
||||
import { readFileSync, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
// Function to manually load .env from project root if process.env is missing keys
|
||||
function loadEnv() {
|
||||
const envPath = join(process.cwd(), ".env");
|
||||
if (existsSync(envPath)) {
|
||||
const envContent = readFileSync(envPath, "utf-8");
|
||||
const lines = envContent.split("\n");
|
||||
for (const line of lines) {
|
||||
if (line && !line.startsWith("#")) {
|
||||
const [key, ...valueParts] = line.split("=");
|
||||
if (key && valueParts.length > 0) {
|
||||
const value = valueParts.join("=").trim().replace(/^["']|["']$/g, "");
|
||||
process.env[key.trim()] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// Ensure environment variables are loaded
|
||||
loadEnv();
|
||||
|
||||
const inputRaw = readFileSync(0, "utf-8");
|
||||
if (!inputRaw) return;
|
||||
|
||||
let finalText = "";
|
||||
let sessionId = "web-desa-darmasaba";
|
||||
|
||||
try {
|
||||
// Try parsing as JSON first
|
||||
const input = JSON.parse(inputRaw);
|
||||
sessionId = input.session_id || "web-desa-darmasaba";
|
||||
finalText = typeof input === "string" ? input : (input.response || input.text || JSON.stringify(input));
|
||||
} catch {
|
||||
// If not JSON, use raw text
|
||||
finalText = inputRaw;
|
||||
}
|
||||
|
||||
const BOT_TOKEN = process.env.BOT_TOKEN;
|
||||
const CHAT_ID = process.env.CHAT_ID;
|
||||
|
||||
if (!BOT_TOKEN || !CHAT_ID) {
|
||||
console.error("Missing BOT_TOKEN or CHAT_ID in environment variables");
|
||||
return;
|
||||
}
|
||||
|
||||
const message =
|
||||
`✅ *Gemini Task Selesai*\n\n` +
|
||||
`🆔 Session: \`${sessionId}\` \n\n` +
|
||||
`🧠 Output:\n${finalText.substring(0, 3500)}`;
|
||||
|
||||
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
chat_id: CHAT_ID,
|
||||
text: message,
|
||||
parse_mode: "Markdown",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
console.error("Telegram API Error:", errorData);
|
||||
} else {
|
||||
console.log("Notification sent successfully!");
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({ status: "continue" }));
|
||||
} catch (err) {
|
||||
console.error("Hook Error:", err);
|
||||
process.stdout.write(JSON.stringify({ status: "continue" }));
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
17
.gemini/settings.json
Normal file
17
.gemini/settings.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"hooks": {
|
||||
"AfterAgent": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"name": "telegram-notify",
|
||||
"type": "command",
|
||||
"command": "bun $GEMINI_PROJECT_DIR/.gemini/hooks/telegram-notify.ts",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
219
.github/workflows/build.yml
vendored
219
.github/workflows/build.yml
vendored
@@ -1,219 +0,0 @@
|
||||
name: Build And Save Log
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: "Target environment (e.g., staging, production)"
|
||||
required: true
|
||||
default: "staging"
|
||||
version:
|
||||
description: "Version to deploy"
|
||||
required: false
|
||||
default: "latest"
|
||||
|
||||
env:
|
||||
APP_NAME: desa-darmasaba-action
|
||||
WA_PHONE: "6289697338821,6289697338822"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:14
|
||||
env:
|
||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd="pg_isready"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=5
|
||||
|
||||
steps:
|
||||
# Checkout kode sumber
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Setup Bun
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
# Cache dependencies
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .bun
|
||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-bun-
|
||||
|
||||
# Step 1: Set BRANCH_NAME based on event type
|
||||
- name: Set BRANCH_NAME
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Step 2: Generate APP_VERSION dynamically
|
||||
- name: Set APP_VERSION
|
||||
run: echo "APP_VERSION=${{ github.sha }}---$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
|
||||
|
||||
# Step 3: Kirim notifikasi ke API build Start
|
||||
- name: Notify start build
|
||||
run: |
|
||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
||||
for PHONE in "${PHONES[@]}"; do
|
||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:start\nApp:${{ env.APP_NAME }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}'))")
|
||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
||||
done
|
||||
|
||||
# Install dependencies
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
# Konfigurasi environment variable untuk PostgreSQL dan variabel tambahan
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
|
||||
echo "PORT=3000" >> .env
|
||||
echo "NEXT_PUBLIC_WIBU_URL=localhost:3000" >> .env
|
||||
echo "WIBU_UPLOAD_DIR=/uploads" >> .env
|
||||
|
||||
# Create log file
|
||||
- name: Create log file
|
||||
run: touch build.txt
|
||||
|
||||
# Migrasi database menggunakan Prisma
|
||||
- name: Apply Prisma schema to database
|
||||
run: bun prisma db push >> build.txt 2>&1
|
||||
|
||||
# Seed database (opsional)
|
||||
- name: Seed database
|
||||
run: |
|
||||
bun prisma db seed >> build.txt 2>&1 || echo "Seed failed or no seed data found. Continuing without seed." >> build.txt
|
||||
|
||||
# Build project
|
||||
- name: Build project
|
||||
run: bun run build >> build.txt 2>&1
|
||||
|
||||
# Ensure project directory exists
|
||||
- name: Ensure /var/www/projects/${{ env.APP_NAME }} exists
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
script: |
|
||||
mkdir -p /var/www/projects/${{ env.APP_NAME }}
|
||||
|
||||
# Deploy to a new version directory
|
||||
- name: Deploy to VPS (New Version)
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
source: "."
|
||||
target: "/var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}"
|
||||
|
||||
# Set up environment variables
|
||||
- name: Set up environment variables
|
||||
run: |
|
||||
rm -r .env
|
||||
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5433/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
|
||||
echo "NEXT_PUBLIC_WIBU_URL=${{ env.APP_NAME }}" >> .env
|
||||
echo "WIBU_UPLOAD_DIR=/var/www/projects/${{ env.APP_NAME }}/uploads" >> .env
|
||||
|
||||
# Kirim file .env ke server
|
||||
- name: Upload .env to server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
source: ".env"
|
||||
target: "/var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}/"
|
||||
|
||||
# manage deployment
|
||||
- name: manage deployment
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.VPS_HOST }}
|
||||
username: ${{ secrets.VPS_USERNAME }}
|
||||
key: ${{ secrets.VPS_SSH_KEY }}
|
||||
script: |
|
||||
|
||||
# Source ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# Find an available port
|
||||
PORT=$(curl -s -X GET https://wibu-bot.wibudev.com/api/find-port | jq -r '.[0]')
|
||||
if [ -z "$PORT" ] || ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
|
||||
echo "Invalid or missing port from API."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# manage deployment
|
||||
cd /var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}
|
||||
|
||||
# Create uploads directory
|
||||
mkdir -p /var/www/projects/${{ env.APP_NAME }}/uploads
|
||||
|
||||
# Install dependencies
|
||||
bun install --production
|
||||
|
||||
# Apply database schema
|
||||
if ! bun prisma db push; then
|
||||
echo "Database migration failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Seed database (optional)
|
||||
bun prisma db seed || echo "tidak membutuhkan seed"
|
||||
|
||||
# Restart the application
|
||||
pm2 reload ${{ env.APP_NAME }} || pm2 start "bun run start --port $PORT" --name "${{ env.APP_NAME }}-$PORT" --namespace "${{ env.APP_NAME }}"
|
||||
|
||||
# Step 4: Set BUILD_STATUS based on success or failure
|
||||
- name: Set BUILD_STATUS
|
||||
if: success()
|
||||
run: echo "BUILD_STATUS=success" >> $GITHUB_ENV
|
||||
|
||||
- name: Set BUILD_STATUS on failure
|
||||
if: failure()
|
||||
run: echo "BUILD_STATUS=failed" >> $GITHUB_ENV
|
||||
|
||||
# Update status log
|
||||
- name: Update status log
|
||||
if: always()
|
||||
run: |
|
||||
echo "=====================" >> build.txt
|
||||
echo "BUILD_STATUS=${{ env.BUILD_STATUS }}" >> build.txt
|
||||
echo "APP_NAME=${{ env.APP_NAME }}" >> build.txt
|
||||
echo "APP_VERSION=${{ env.APP_VERSION }}" >> build.txt
|
||||
echo "=====================" >> build.txt
|
||||
|
||||
# Upload log to 0x0.st
|
||||
- name: Upload log to 0x0.st
|
||||
id: upload_log
|
||||
if: always()
|
||||
run: |
|
||||
LOG_URL=$(curl -F "file=@build.txt" https://wibu-bot.wibudev.com/api/file )
|
||||
echo "LOG_URL=$LOG_URL" >> $GITHUB_ENV
|
||||
|
||||
# Kirim notifikasi ke API
|
||||
- name: Notify build success via API
|
||||
if: always()
|
||||
run: |
|
||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
||||
for PHONE in "${PHONES[@]}"; do
|
||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:${{ env.BUILD_STATUS }}\nApp:${{ env.APP_NAME }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}\nLog:${{ env.LOG_URL }}'))")
|
||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
||||
done
|
||||
56
.github/workflows/docker-publish.yml
vendored
Normal file
56
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Publish Docker to GHCR
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
jobs:
|
||||
publish:
|
||||
name: Build & Push to GHCR
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
sudo docker image prune --all --force
|
||||
df -h
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract tag name
|
||||
id: meta
|
||||
run: echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
106
.github/workflows/publish.yml
vendored
Normal file
106
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: Publish Docker to GHCR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stack_env:
|
||||
description: "stack env"
|
||||
required: true
|
||||
type: choice
|
||||
default: "dev"
|
||||
options:
|
||||
- dev
|
||||
- prod
|
||||
- stg
|
||||
tag:
|
||||
description: "Image tag (e.g. 1.0.0)"
|
||||
required: true
|
||||
default: "1.0.0"
|
||||
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Free disk space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||
sudo docker image prune --all --force
|
||||
df -h
|
||||
|
||||
- name: Checkout branch ${{ github.event.inputs.stack_env }}
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.stack_env }}
|
||||
|
||||
- name: Checkout scripts from main
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
path: .ci
|
||||
sparse-checkout: .github/workflows/script
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate image metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=raw,value=${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
|
||||
type=raw,value=${{ github.event.inputs.stack_env }}-latest
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
no-cache: true
|
||||
|
||||
- name: Notify success
|
||||
if: success()
|
||||
run: bash ./.ci/.github/workflows/script/notify.sh
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NOTIFY_STATUS: success
|
||||
NOTIFY_WORKFLOW: "Publish Docker"
|
||||
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
|
||||
|
||||
- name: Notify failure
|
||||
if: failure()
|
||||
run: bash ./.ci/.github/workflows/script/notify.sh
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NOTIFY_STATUS: failure
|
||||
NOTIFY_WORKFLOW: "Publish Docker"
|
||||
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
|
||||
60
.github/workflows/re-pull.yml
vendored
Normal file
60
.github/workflows/re-pull.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
name: Re-Pull Docker
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
stack_name:
|
||||
description: "stack name"
|
||||
required: true
|
||||
type: string
|
||||
stack_env:
|
||||
description: "stack env"
|
||||
required: true
|
||||
type: choice
|
||||
default: "dev"
|
||||
options:
|
||||
- dev
|
||||
- stg
|
||||
- prod
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Re-Pull Docker ${{ github.event.inputs.stack_name }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout scripts from main
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
sparse-checkout: .github/workflows/script
|
||||
|
||||
- name: Deploy ke Portainer
|
||||
run: bash ./.github/workflows/script/re-pull.sh
|
||||
env:
|
||||
PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }}
|
||||
PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }}
|
||||
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
|
||||
STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}
|
||||
|
||||
- name: Notify success
|
||||
if: success()
|
||||
run: bash ./.github/workflows/script/notify.sh
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NOTIFY_STATUS: success
|
||||
NOTIFY_WORKFLOW: "Re-Pull Docker"
|
||||
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
|
||||
|
||||
- name: Notify failure
|
||||
if: failure()
|
||||
run: bash ./.github/workflows/script/notify.sh
|
||||
env:
|
||||
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
NOTIFY_STATUS: failure
|
||||
NOTIFY_WORKFLOW: "Re-Pull Docker"
|
||||
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
|
||||
26
.github/workflows/script/notify.sh
vendored
Normal file
26
.github/workflows/script/notify.sh
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}"
|
||||
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}"
|
||||
: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}"
|
||||
: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}"
|
||||
|
||||
if [ "$NOTIFY_STATUS" = "success" ]; then
|
||||
ICON="✅"
|
||||
TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!"
|
||||
else
|
||||
ICON="❌"
|
||||
TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!"
|
||||
fi
|
||||
|
||||
if [ -n "$NOTIFY_DETAIL" ]; then
|
||||
TEXT="${TEXT}
|
||||
${NOTIFY_DETAIL}"
|
||||
fi
|
||||
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n \
|
||||
--arg chat_id "$TELEGRAM_CHAT_ID" \
|
||||
--arg text "$TEXT" \
|
||||
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')"
|
||||
120
.github/workflows/script/re-pull.sh
vendored
Normal file
120
.github/workflows/script/re-pull.sh
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
#!/bin/bash
|
||||
|
||||
: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}"
|
||||
: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}"
|
||||
: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}"
|
||||
: "${STACK_NAME:?STACK_NAME tidak di-set}"
|
||||
|
||||
# Timeout total: MAX_RETRY * SLEEP_INTERVAL detik
|
||||
MAX_RETRY=60 # 60 × 10s = 10 menit
|
||||
SLEEP_INTERVAL=10
|
||||
|
||||
echo "🔐 Autentikasi ke Portainer..."
|
||||
TOKEN=$(curl -s -X POST "https://${PORTAINER_URL}/api/auth" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \
|
||||
| jq -r .jwt)
|
||||
|
||||
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||
echo "❌ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🔍 Mencari stack: $STACK_NAME..."
|
||||
STACK=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
| jq ".[] | select(.Name == \"$STACK_NAME\")")
|
||||
|
||||
if [ -z "$STACK" ]; then
|
||||
echo "❌ Stack '$STACK_NAME' tidak ditemukan di Portainer!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STACK_ID=$(echo "$STACK" | jq -r .Id)
|
||||
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
|
||||
ENV=$(echo "$STACK" | jq '.Env // []')
|
||||
|
||||
# ── Catat container ID lama sebelum redeploy ──────────────────────────────────
|
||||
echo "📸 Mencatat container aktif sebelum redeploy..."
|
||||
CONTAINERS_BEFORE=$(curl -s -X GET \
|
||||
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
|
||||
-H "Authorization: Bearer ${TOKEN}")
|
||||
|
||||
OLD_IDS=$(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Id] | join(",")')
|
||||
echo " Container lama: $(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Names[0]] | join(", ")')"
|
||||
|
||||
# ── Ambil compose file lalu trigger redeploy ─────────────────────────────────
|
||||
echo "📄 Mengambil compose file..."
|
||||
STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
| jq -r .StackFileContent)
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg content "$STACK_FILE" \
|
||||
--argjson env "$ENV" \
|
||||
'{stackFileContent: $content, env: $env, pullImage: true}')
|
||||
|
||||
echo "🚀 Triggering redeploy $STACK_NAME (pull latest image)..."
|
||||
HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \
|
||||
-X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
if [ "$HTTP_STATUS" != "200" ]; then
|
||||
echo "❌ Redeploy gagal! HTTP Status: $HTTP_STATUS"
|
||||
cat /tmp/portainer_response.json | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "⏳ Menunggu image selesai di-pull dan container baru running..."
|
||||
echo " (Timeout: $((MAX_RETRY * SLEEP_INTERVAL)) detik)"
|
||||
|
||||
COUNT=0
|
||||
while [ $COUNT -lt $MAX_RETRY ]; do
|
||||
sleep $SLEEP_INTERVAL
|
||||
COUNT=$((COUNT + 1))
|
||||
|
||||
CONTAINERS=$(curl -s -X GET \
|
||||
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
|
||||
-H "Authorization: Bearer ${TOKEN}")
|
||||
|
||||
# Container baru = ID tidak ada di daftar container lama
|
||||
NEW_RUNNING=$(echo "$CONTAINERS" | jq \
|
||||
--arg old "$OLD_IDS" \
|
||||
'[.[] | select(.State == "running" and ((.Id) as $id | ($old | split(",") | index($id)) == null))] | length')
|
||||
|
||||
FAILED=$(echo "$CONTAINERS" | jq \
|
||||
'[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not))] | length')
|
||||
|
||||
echo "🔄 [$((COUNT * SLEEP_INTERVAL))s / $((MAX_RETRY * SLEEP_INTERVAL))s] Container baru running: ${NEW_RUNNING} | Gagal: ${FAILED}"
|
||||
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status) | id: \(.Id[:12])"'
|
||||
|
||||
if [ "$FAILED" -gt "0" ]; then
|
||||
echo ""
|
||||
echo "❌ Ada container yang crash!"
|
||||
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not)) | " → \(.Names[0]) | \(.Status)"'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$NEW_RUNNING" -gt "0" ]; then
|
||||
# Cleanup dangling images setelah redeploy sukses
|
||||
echo "🧹 Membersihkan dangling images..."
|
||||
curl -s -X POST "https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/images/prune" \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"filters":{"dangling":["true"]}}' | jq -r '" Reclaimed: \(.SpaceReclaimed // 0 | . / 1073741824 | tostring | .[0:5]) GB"'
|
||||
|
||||
echo "✅ Cleanup selesai!"
|
||||
echo ""
|
||||
echo "✅ Stack $STACK_NAME berhasil di-redeploy dengan image baru dan running!"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "❌ Timeout $((MAX_RETRY * SLEEP_INTERVAL))s! Container baru tidak kunjung running."
|
||||
echo " Kemungkinan image masih dalam proses pull atau ada error di server."
|
||||
exit 1
|
||||
55
.github/workflows/test.yml
vendored
55
.github/workflows/test.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: test workflows
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: "Target environment (e.g., staging, production)"
|
||||
required: true
|
||||
default: "staging"
|
||||
version:
|
||||
description: "Version to deploy"
|
||||
required: false
|
||||
default: "latest"
|
||||
|
||||
env:
|
||||
APP_NAME: desa-darmasaba-action
|
||||
WA_PHONE: "6289697338821,6289697338822"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checkout kode sumber
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Setup Bun
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
# Create log file
|
||||
- name: Create log file
|
||||
run: touch build.txt
|
||||
|
||||
# Step 1: Set BRANCH_NAME based on event type
|
||||
- name: Set BRANCH_NAME
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Step 2: Generate APP_VERSION dynamically
|
||||
- name: Set APP_VERSION
|
||||
run: echo "APP_VERSION=${{ github.sha }}---$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
|
||||
|
||||
# Step 3: Kirim notifikasi ke API build Start
|
||||
- name: Notify start build
|
||||
run: |
|
||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
||||
for PHONE in "${PHONES[@]}"; do
|
||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:start\nApp:${{ env.APP_NAME }}\nenv:${{ inputs.environment }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}'))")
|
||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
||||
done
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -29,7 +29,12 @@ yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env
|
||||
# env local files (keep .env.example)
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
# QC
|
||||
QC
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -41,10 +46,12 @@ next-env.d.ts
|
||||
# uploads
|
||||
/uploads
|
||||
|
||||
# download
|
||||
/download
|
||||
|
||||
# cache
|
||||
/cache
|
||||
|
||||
.github/
|
||||
|
||||
.env.*
|
||||
*.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)
|
||||
68
Dockerfile
Normal file
68
Dockerfile
Normal file
@@ -0,0 +1,68 @@
|
||||
# ==============================
|
||||
# Stage 1: Builder
|
||||
# ==============================
|
||||
FROM oven/bun:1-debian AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libc6 \
|
||||
git \
|
||||
openssl \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package.json bun.lockb* ./
|
||||
|
||||
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN cp .env.example .env || true
|
||||
|
||||
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
|
||||
RUN bunx prisma generate
|
||||
|
||||
# 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.* ./
|
||||
COPY --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["/app/docker-entrypoint.sh"]
|
||||
62
GEMINI.md
Normal file
62
GEMINI.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Project: Desa Darmasaba
|
||||
|
||||
## Project Overview
|
||||
|
||||
The `desa-darmasaba` project is a Next.js (version 15+) application developed with TypeScript. It serves as an official platform for Desa Darmasaba (a village in Badung, Bali), offering various public services, news, and detailed village profiles.
|
||||
|
||||
**Key Technologies:**
|
||||
|
||||
* **Frontend Framework:** Next.js (v15+) with React (v19+)
|
||||
* **Language:** TypeScript
|
||||
* **UI Library:** Mantine UI
|
||||
* **Database ORM:** Prisma (v6+)
|
||||
* **Database:** PostgreSQL (as configured in `prisma/schema.prisma`)
|
||||
* **API Framework:** Elysia (used for API routes, as seen in dependencies)
|
||||
* **State Management:** Potentially Jotai and Valtio (listed in dependencies)
|
||||
* **Image Processing:** Sharp
|
||||
* **Package Manager:** Likely Bun, given `bun.lockb` and the `prisma:seed` script.
|
||||
|
||||
The application architecture follows the Next.js App Router structure, with comprehensive data models defined in `prisma/schema.prisma` covering various domains like public information, health, security, economy, innovation, environment, and education. It also includes configurations for image handling and caching.
|
||||
|
||||
## Building and Running
|
||||
|
||||
This project uses `bun` as the package manager. Ensure Bun is installed to run these commands.
|
||||
|
||||
* **Install Dependencies:**
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
* **Development Server:**
|
||||
Runs the Next.js development server.
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
* **Build for Production:**
|
||||
Builds the Next.js application for production deployment.
|
||||
```bash
|
||||
bun run build
|
||||
```
|
||||
|
||||
* **Start Production Server:**
|
||||
Starts the Next.js application in production mode.
|
||||
```bash
|
||||
bun run start
|
||||
```
|
||||
|
||||
* **Database Seeding:**
|
||||
Executes the Prisma seeding script to populate the database.
|
||||
```bash
|
||||
bun run prisma:seed
|
||||
```
|
||||
|
||||
## Development Conventions
|
||||
|
||||
* **Coding Language:** TypeScript is strictly enforced.
|
||||
* **Frontend Framework:** Next.js App Router for page and component structuring.
|
||||
* **UI/UX:** Adherence to Mantine UI component library for consistent styling and user experience.
|
||||
* **Database Interaction:** Prisma ORM is used for all database operations, with a PostgreSQL database.
|
||||
* **Linting:** ESLint is configured with `next/core-web-vitals` and `next/typescript` to maintain code quality and adherence to Next.js and TypeScript best practices.
|
||||
* **Styling:** PostCSS is used, with `postcss-preset-mantine` and `postcss-simple-vars` defining Mantine-specific breakpoints and other CSS variables.
|
||||
* **Imports:** Absolute imports are configured using `@/*` which resolves to the `src/` directory.
|
||||
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
|
||||
273
QWEN.md
Normal file
273
QWEN.md
Normal file
@@ -0,0 +1,273 @@
|
||||
# Desa Darmasaba - Village Management System
|
||||
|
||||
## Project Overview
|
||||
|
||||
Desa Darmasaba is a comprehensive Next.js 15 application designed for village management services in Darmasaba, Badung, Bali. The application serves as a digital platform for government services, public information, and community engagement. It features multiple sections including PPID (Public Information Disclosure), health services, security, education, environment, economy, innovation, and more.
|
||||
|
||||
### Key Technologies
|
||||
- **Framework**: Next.js 15 with App Router
|
||||
- **Language**: TypeScript with strict mode
|
||||
- **Styling**: Mantine UI components with custom CSS
|
||||
- **Backend**: Elysia.js API server integrated with Next.js
|
||||
- **Database**: PostgreSQL with Prisma ORM
|
||||
- **State Management**: Valtio for global state
|
||||
- **Authentication**: JWT with iron-session
|
||||
|
||||
### Architecture
|
||||
The application follows a modular architecture with:
|
||||
- A main frontend built with Next.js and Mantine UI
|
||||
- An integrated Elysia.js API server for backend operations
|
||||
- Prisma ORM for database interactions
|
||||
- File storage integration with Seafile
|
||||
- Multiple domain-specific modules (PPID, health, security, education, etc.)
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (with Bun runtime)
|
||||
- PostgreSQL database
|
||||
- Seafile server for file storage
|
||||
|
||||
### Setup Instructions
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
2. Set up environment variables in `.env.local`:
|
||||
```
|
||||
DATABASE_URL=your_postgresql_connection_string
|
||||
SEAFILE_TOKEN=your_seafile_token
|
||||
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||
SEAFILE_BASE_URL=your_seafile_base_url
|
||||
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||
SEAFILE_URL=your_seafile_api_url
|
||||
WIBU_UPLOAD_DIR=your_upload_directory
|
||||
```
|
||||
|
||||
3. Generate Prisma client:
|
||||
```bash
|
||||
bunx prisma generate
|
||||
```
|
||||
|
||||
4. Push database schema:
|
||||
```bash
|
||||
bunx prisma db push
|
||||
```
|
||||
|
||||
5. Seed the database:
|
||||
```bash
|
||||
bun run prisma/seed.ts
|
||||
```
|
||||
|
||||
6. Run the development server:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
### Available Scripts
|
||||
- `bun run dev` - Start development server
|
||||
- `bun run build` - Build for production
|
||||
- `bun run start` - Start production server
|
||||
- `bun run prisma/seed.ts` - Run database seeding
|
||||
- `bunx prisma generate` - Generate Prisma client
|
||||
- `bunx prisma db push` - Push schema changes to database
|
||||
- `bunx prisma studio` - Open Prisma Studio GUI
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Code Structure
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js app router pages
|
||||
│ ├── admin/ # Admin dashboard pages
|
||||
│ ├── api/ # API routes with Elysia.js
|
||||
│ ├── darmasaba/ # Public-facing village pages
|
||||
│ └── ...
|
||||
├── con/ # Constants and configuration
|
||||
├── hooks/ # React hooks
|
||||
├── lib/ # Utility functions and configurations
|
||||
├── middlewares/ # Next.js middleware
|
||||
├── state/ # Global state management
|
||||
├── store/ # Additional state management
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Utility functions
|
||||
```
|
||||
|
||||
### Import Conventions
|
||||
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
||||
- Group imports: external libraries first, then internal modules
|
||||
- Keep import statements organized and remove unused imports
|
||||
|
||||
```typescript
|
||||
// External libraries
|
||||
import { useState } from 'react'
|
||||
import { Button, Stack } from '@mantine/core'
|
||||
|
||||
// Internal modules
|
||||
import ApiFetch from '@/lib/api-fetch'
|
||||
import { MyComponent } from '@/components/my-component'
|
||||
```
|
||||
|
||||
### TypeScript Configuration
|
||||
- Strict mode enabled (`"strict": true`)
|
||||
- Target: ES2017
|
||||
- Module resolution: bundler
|
||||
- Path alias: `@/*` maps to `./src/*`
|
||||
|
||||
### Naming Conventions
|
||||
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
|
||||
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
|
||||
- **Variables/Functions**: camelCase
|
||||
- **Constants**: UPPER_SNAKE_CASE
|
||||
- **Database Models**: PascalCase (Prisma convention)
|
||||
|
||||
### Error Handling
|
||||
- Use try-catch blocks for async operations
|
||||
- Implement proper error boundaries in React components
|
||||
- Log errors appropriately without exposing sensitive data
|
||||
- Use Zod for runtime validation and type safety
|
||||
|
||||
### API Structure
|
||||
- Backend uses Elysia.js with TypeScript
|
||||
- API routes are in `src/app/api/[[...slugs]]/` directory
|
||||
- Use treaty client for type-safe API calls
|
||||
- Follow RESTful conventions for endpoints
|
||||
- Include proper HTTP status codes and error responses
|
||||
|
||||
### Database Operations
|
||||
- Use Prisma client from `@/lib/prisma.ts`
|
||||
- Database connection includes graceful shutdown handling
|
||||
- Use transactions for complex operations
|
||||
- Implement proper error handling for database queries
|
||||
|
||||
### Component Guidelines
|
||||
- Use functional components with hooks
|
||||
- Implement proper prop types with TypeScript interfaces
|
||||
- Use Mantine components for UI consistency
|
||||
- Follow atomic design principles when possible
|
||||
- Add loading states and error states for async operations
|
||||
|
||||
### State Management
|
||||
- Use Valtio proxies for global state
|
||||
- Keep local state in components when possible
|
||||
- Use SWR for server state caching
|
||||
- Implement optimistic updates for better UX
|
||||
|
||||
### Styling
|
||||
- Primary: Mantine UI components
|
||||
- Use Mantine theme system for customization
|
||||
- Custom CSS should be minimal and scoped
|
||||
- Follow responsive design principles
|
||||
- Use semantic HTML5 elements
|
||||
|
||||
### Security Practices
|
||||
- Validate all user inputs with Zod schemas
|
||||
- Use JWT tokens for authentication
|
||||
- Implement proper CORS configuration
|
||||
- Never expose database credentials or API keys
|
||||
- Use HTTPS in production
|
||||
- Implement rate limiting for sensitive endpoints
|
||||
|
||||
### Performance Considerations
|
||||
- Use Next.js Image optimization
|
||||
- Implement proper caching strategies
|
||||
- Use React.memo for expensive components
|
||||
- Optimize bundle size with dynamic imports
|
||||
- Use Prisma query optimization
|
||||
|
||||
## Domain Modules
|
||||
|
||||
The application is organized into several domain modules:
|
||||
|
||||
1. **PPID (Public Information Disclosure)**: Profile, structure, information requests, legal basis
|
||||
2. **Health**: Health facilities, programs, emergency response, disease information
|
||||
3. **Security**: Community security, emergency contacts, crime prevention
|
||||
4. **Education**: Schools, scholarships, educational programs
|
||||
5. **Economy**: Local markets, BUMDes, employment data
|
||||
6. **Environment**: Environmental data, conservation, waste management
|
||||
7. **Innovation**: Digital services, innovation programs
|
||||
8. **Culture**: Village traditions, music, cultural preservation
|
||||
|
||||
Each module has its own section in both the admin panel and public-facing areas.
|
||||
|
||||
## File Storage Integration
|
||||
|
||||
The application integrates with Seafile for file storage, with specific handling for:
|
||||
- Images and documents
|
||||
- Public sharing capabilities
|
||||
- CDN URL generation
|
||||
- Batch processing of assets
|
||||
|
||||
## Testing
|
||||
|
||||
Currently no formal test framework is configured. When adding tests:
|
||||
- Consider Jest or Vitest for unit testing
|
||||
- Use Playwright for E2E testing
|
||||
- Update this section with specific test commands
|
||||
|
||||
## Deployment
|
||||
|
||||
The application includes deployment scripts in the `NOTE.md` file that outline:
|
||||
- Automated deployment with GitHub API integration
|
||||
- Environment-specific configurations
|
||||
- PM2 process management
|
||||
- Release management with versioning
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
- **API endpoints returning 404**: Check that environment variables are properly configured
|
||||
- **Database connection errors**: Verify DATABASE_URL in environment variables
|
||||
- **File upload issues**: Ensure Seafile integration is properly configured
|
||||
- **Build failures**: Run `bunx prisma generate` before building
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. Always run type checking before committing: `bunx tsc --noEmit`
|
||||
2. Run linting to catch style issues: `bun run eslint .`
|
||||
3. Test database changes with `bunx prisma db push`
|
||||
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
||||
5. Check environment variables are properly configured
|
||||
6. Verify responsive design on different screen sizes
|
||||
|
||||
## Qwen Added Memories
|
||||
- **GitHub Workflow Execution**: Project ini memiliki 3 workflow GitHub Action:
|
||||
1. `publish.yml` - Build & push Docker image ke GHCR (manual trigger, butuh input: stack_env + tag)
|
||||
2. `re-pull.yml` - Re-pull Docker image di Portainer (manual trigger, butuh input: stack_name + stack_env)
|
||||
3. `docker-publish.yml` - Auto build & push saat ada tag versi v*
|
||||
|
||||
Workflow bisa dijalankan via GitHub CLI: `gh workflow run <nama.yml> -f param=value --ref branch`
|
||||
|
||||
Setelah commit ke branch deployment (dev/stg/prod), otomatis trigger workflow publish + re-pull untuk deploy ke server.
|
||||
- **Deployment Workflow Sistematis**:
|
||||
1. **Version Bump** - Update `version` di `package.json` sebelum deploy (ikuti semver: major.minor.patch)
|
||||
2. **Commit** - Commit perubahan + version bump dengan pesan yang jelas
|
||||
3. **Push ke Branch** - Push ke branch target (biasanya `stg` untuk staging atau `prod` untuk production)
|
||||
4. **Trigger Publish** - Jalankan `gh workflow run publish.yml --ref <branch> -f stack_env=<env> -f tag=<version>`
|
||||
5. **Trigger Re-Pull** - Jalankan `gh workflow run re-pull.yml -f stack_name=desa-darmasaba -f stack_env=<env>`
|
||||
6. **Verifikasi** - Cek workflow berhasil dan aplikasi berjalan
|
||||
|
||||
Branch deployment: `stg` (staging) atau `prod` (production)
|
||||
Version format di package.json: `"version": "major.minor.patch"`
|
||||
- **Deployment Workflow HARUS Sequential (Berurutan)**:
|
||||
|
||||
Saat deploy ke stg atau prod, workflow TIDAK BOLEH dijalankan bersamaan. Harus menunggu yang pertama SELESAI total baru trigger yang kedua.
|
||||
|
||||
**Urutan yang BENAR:**
|
||||
1. ✅ **publish.yml** - Tunggu sampai SELESAI (status: ✓ success)
|
||||
2. ✅ **Setelah publish selesai**, baru trigger **re-pull.yml**
|
||||
|
||||
**JANGAN trigger keduanya bersamaan!** Ini akan menyebabkan race condition karena re-pull akan menarik image yang belum selesai di-build.
|
||||
|
||||
**Cara cek workflow selesai:**
|
||||
```bash
|
||||
gh run view <run_id> --json status --jq '.status'
|
||||
# Harus return "completed" baru lanjut ke re-pull
|
||||
```
|
||||
|
||||
**Atau polling sampai selesai:**
|
||||
```bash
|
||||
gh run watch <publish_run_id>
|
||||
# Tunggu sampai ada checkmark ✓
|
||||
```
|
||||
30
__tests__/api/fileStorage.test.ts
Normal file
30
__tests__/api/fileStorage.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import ApiFetch from '@/lib/api-fetch';
|
||||
|
||||
describe('FileStorage API', () => {
|
||||
it('should fetch a list of files from /api/fileStorage/findMany', async () => {
|
||||
const response = await ApiFetch.api.fileStorage.findMany.get();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const responseBody = response.data as any;
|
||||
|
||||
expect(responseBody.data).toBeInstanceOf(Array);
|
||||
expect(responseBody.data.length).toBe(2);
|
||||
expect(responseBody.data[0].name).toBe('file1.jpg');
|
||||
});
|
||||
|
||||
it('should create a file using /api/fileStorage/create', async () => {
|
||||
const mockFile = new File(['hello'], 'hello.png', { type: 'image/png' });
|
||||
const response = await ApiFetch.api.fileStorage.create.post({
|
||||
file: mockFile,
|
||||
name: 'hello.png',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const responseBody = response.data as any;
|
||||
|
||||
expect(responseBody.data.realName).toBe('hello.png');
|
||||
expect(responseBody.data.id).toBe('3');
|
||||
});
|
||||
});
|
||||
11
__tests__/e2e/homepage.spec.ts
Normal file
11
__tests__/e2e/homepage.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('homepage has correct title and content', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Wait for the redirect to /darmasaba
|
||||
await page.waitForURL('/darmasaba');
|
||||
|
||||
// Check for the main heading
|
||||
await expect(page.getByText('DARMASABA', { exact: true })).toBeVisible();
|
||||
});
|
||||
43
__tests__/mocks/handlers.ts
Normal file
43
__tests__/mocks/handlers.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const handlers = [
|
||||
http.get('http://localhost:3000/api/fileStorage/findMany', () => {
|
||||
return HttpResponse.json({
|
||||
data: [
|
||||
{ id: '1', name: 'file1.jpg', url: '/uploads/file1.jpg' },
|
||||
{ id: '2', name: 'file2.png', url: '/uploads/file2.png' },
|
||||
],
|
||||
meta: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 2,
|
||||
totalPages: 1,
|
||||
},
|
||||
});
|
||||
}),
|
||||
http.post('http://localhost:3000/api/fileStorage/create', async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
const file = data.get('file') as File;
|
||||
const name = data.get('name') as string;
|
||||
|
||||
if (!file) {
|
||||
return new HttpResponse(null, { status: 400 });
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
id: '3',
|
||||
name: 'generated-nanoid',
|
||||
path: `/uploads/generated-nanoid`,
|
||||
link: `/uploads/generated-nanoid`,
|
||||
realName: name,
|
||||
mimeType: file.type,
|
||||
category: "uncategorized",
|
||||
isActive: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
}
|
||||
});
|
||||
}),
|
||||
];
|
||||
4
__tests__/mocks/server.ts
Normal file
4
__tests__/mocks/server.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { handlers } from './handlers';
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
7
__tests__/setup.ts
Normal file
7
__tests__/setup.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { server } from './mocks/server';
|
||||
import { beforeAll, afterEach, afterAll } from 'vitest';
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
49
biome.json
Normal file
49
biome.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"experimentalScannerIgnores": [
|
||||
"node_modules",
|
||||
".next",
|
||||
"out",
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"noUnusedVariables": "warn",
|
||||
"noUnusedImports": "warn"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "warn"
|
||||
},
|
||||
"complexity": {
|
||||
"noForEach": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "always"
|
||||
}
|
||||
}
|
||||
}
|
||||
169
darkMode.md
Normal file
169
darkMode.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# 🌙 Dark Mode Design Specification
|
||||
## Admin Darmasaba – Dashboard & CMS
|
||||
|
||||
Dokumen ini mendefinisikan standar **Dark Mode UI** agar:
|
||||
- nyaman di mata
|
||||
- konsisten
|
||||
- tidak flat
|
||||
- tetap profesional untuk aplikasi pemerintahan
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Color Palette (Dark Mode)
|
||||
|
||||
### Background Layers
|
||||
| Layer | Token | Warna | Fungsi |
|
||||
|------|------|------|------|
|
||||
| Base | `--bg-base` | `#0B1220` | Background utama aplikasi |
|
||||
| App | `--bg-app` | `#0F172A` | Area kerja utama |
|
||||
| Card | `--bg-card` | `#162235` | Card / container |
|
||||
| Surface | `--bg-surface` | `#1E2A3D` | Table header, tab, input |
|
||||
|
||||
---
|
||||
|
||||
### Border & Divider
|
||||
| Token | Warna | Catatan |
|
||||
|-----|------|--------|
|
||||
| `--border-default` | `#2A3A52` | Border utama |
|
||||
| `--border-soft` | `#22314A` | Divider halus |
|
||||
|
||||
> ❗ Hindari border terlalu tipis (`opacity < 20%`)
|
||||
|
||||
---
|
||||
|
||||
### Text Colors
|
||||
| Jenis | Token | Warna |
|
||||
|-----|------|------|
|
||||
| Primary | `--text-primary` | `#E5E7EB` |
|
||||
| Secondary | `--text-secondary` | `#9CA3AF` |
|
||||
| Muted | `--text-muted` | `#6B7280` |
|
||||
| Inverse | `--text-inverse` | `#020617` |
|
||||
|
||||
---
|
||||
|
||||
### Accent & Action
|
||||
| Fungsi | Warna |
|
||||
|------|------|
|
||||
| Primary Action | `#3B82F6` |
|
||||
| Hover | `#2563EB` |
|
||||
| Active | `#1D4ED8` |
|
||||
| Link | `#60A5FA` |
|
||||
|
||||
---
|
||||
|
||||
### Status Colors
|
||||
| Status | Warna |
|
||||
|------|------|
|
||||
| Success | `#22C55E` |
|
||||
| Warning | `#FACC15` |
|
||||
| Error | `#EF4444` |
|
||||
| Info | `#38BDF8` |
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Layout Rules
|
||||
|
||||
### Sidebar
|
||||
- Background: `--bg-app`
|
||||
- Active menu:
|
||||
- Background: `rgba(59,130,246,0.15)`
|
||||
- Text: Primary
|
||||
- Indicator: kiri (2–3px accent bar)
|
||||
- Hover:
|
||||
- Background: `rgba(255,255,255,0.04)`
|
||||
|
||||
---
|
||||
|
||||
### Header / Topbar
|
||||
- Background: `linear-gradient(#0F172A → #0B1220)`
|
||||
- Border bawah wajib (`--border-soft`)
|
||||
- Icon:
|
||||
- Default: muted
|
||||
- Hover: primary
|
||||
|
||||
---
|
||||
|
||||
## 📦 Card & Section
|
||||
|
||||
### Card
|
||||
- Background: `--bg-card`
|
||||
- Border: `--border-default`
|
||||
- Radius: 12–16px
|
||||
- Jangan pakai shadow hitam
|
||||
|
||||
### Section Header
|
||||
- Font weight lebih besar
|
||||
- Text: primary
|
||||
- Spacing jelas dari konten
|
||||
|
||||
---
|
||||
|
||||
## 📊 Table (Dark Mode Friendly)
|
||||
|
||||
### Table Header
|
||||
- Background: `--bg-surface`
|
||||
- Text: secondary
|
||||
- Font weight: medium
|
||||
|
||||
### Table Row
|
||||
- Default: transparent
|
||||
- Hover:
|
||||
- Background: `rgba(255,255,255,0.03)`
|
||||
- Divider antar row wajib terlihat
|
||||
|
||||
### Link di Table
|
||||
- Warna link **lebih terang dari text**
|
||||
- Hover underline
|
||||
|
||||
---
|
||||
|
||||
## 🔘 Button Rules
|
||||
|
||||
### Primary Button
|
||||
- Background: Primary Action
|
||||
- Text: Inverse
|
||||
- Hover: darker shade
|
||||
|
||||
### Secondary Button
|
||||
- Background: transparent
|
||||
- Border: `--border-default`
|
||||
- Text: primary
|
||||
|
||||
### Icon Button
|
||||
- Default: muted
|
||||
- Hover: primary + bg soft
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Tab Navigation
|
||||
|
||||
- Inactive:
|
||||
- Text: muted
|
||||
- Active:
|
||||
- Background: `rgba(59,130,246,0.15)`
|
||||
- Text: primary
|
||||
- Icon ikut berubah
|
||||
|
||||
---
|
||||
|
||||
## 🌗 Dark vs Light Mode Rule
|
||||
- Layout, spacing, typography **HARUS SAMA**
|
||||
- Yang boleh beda:
|
||||
- warna
|
||||
- border intensity
|
||||
- background layer
|
||||
|
||||
> ❌ Jangan ganti struktur UI antara dark & light
|
||||
|
||||
---
|
||||
|
||||
## ✅ Dark Mode Checklist
|
||||
- [ ] Kontras teks terbaca
|
||||
- [ ] Active state jelas
|
||||
- [ ] Hover terasa hidup
|
||||
- [ ] Tidak flat
|
||||
- [ ] Tidak silau
|
||||
|
||||
---
|
||||
|
||||
Dokumen ini adalah **single source of truth** untuk Dark Mode.
|
||||
553
dev-inspector-click-to-source.md
Normal file
553
dev-inspector-click-to-source.md
Normal file
@@ -0,0 +1,553 @@
|
||||
# Skill: Dev Inspector — Click-to-Source untuk Bun + Elysia + Vite + React
|
||||
|
||||
## Ringkasan
|
||||
|
||||
Fitur development: klik elemen UI di browser → langsung buka source code di editor (VS Code, Cursor, dll) pada baris dan kolom yang tepat. Zero overhead di production.
|
||||
|
||||
**Hotkey**: `Ctrl+Shift+Cmd+C` (macOS) / `Ctrl+Shift+Alt+C` → aktifkan mode inspect → klik elemen → file terbuka.
|
||||
|
||||
## Kenapa Tidak Pakai Library
|
||||
|
||||
`react-dev-inspector` crash di React 19 karena:
|
||||
- `fiber.return.child.sibling` bisa null di React 19
|
||||
- `_debugSource` dihapus dari React 19
|
||||
- Walking fiber tree tidak stabil antar versi React
|
||||
|
||||
Solusi ini **regex-based + multi-fallback**, tidak bergantung pada React internals.
|
||||
|
||||
## Syarat Arsitektur
|
||||
|
||||
Fitur ini bekerja karena 4 syarat struktural terpenuhi. Jika salah satu tidak ada, fitur tidak bisa diimplementasi atau perlu adaptasi signifikan.
|
||||
|
||||
### 1. Vite sebagai Bundler (Wajib)
|
||||
|
||||
Seluruh mekanisme bergantung pada **Vite plugin transform pipeline**:
|
||||
- `inspectorPlugin()` inject attributes ke JSX saat build/HMR
|
||||
- `enforce: 'pre'` memastikan plugin jalan sebelum OXC/Babel transform JSX
|
||||
- `import.meta.env?.DEV` sebagai compile-time constant untuk tree-shaking
|
||||
|
||||
**Tidak bisa diganti dengan**: esbuild standalone, webpack (perlu loader berbeda), SWC standalone.
|
||||
**Bisa diganti dengan**: framework yang pakai Vite di dalamnya (Remix Vite, TanStack Start, Astro).
|
||||
|
||||
### 2. Server dan Frontend dalam Satu Proses (Wajib)
|
||||
|
||||
Endpoint `/__open-in-editor` harus **satu proses dengan dev server** yang melayani frontend:
|
||||
- Browser POST ke origin yang sama (no CORS)
|
||||
- Server punya akses ke filesystem lokal untuk `Bun.spawn(editor)`
|
||||
- Endpoint harus bisa ditangani **sebelum routing & middleware** (auth, tenant, dll)
|
||||
|
||||
**Pola yang memenuhi syarat:**
|
||||
- Elysia + Vite middlewareMode (project ini) — `onRequest` intercept sebelum route matching
|
||||
- Express/Fastify + Vite middlewareMode — middleware biasa sebelum auth
|
||||
- Vite dev server standalone (`vite dev`) — pakai `configureServer` hook
|
||||
|
||||
**Tidak memenuhi syarat:**
|
||||
- Frontend dan backend di proses/port terpisah (misal: CRA + separate API server) — perlu proxy atau CORS config tambahan
|
||||
- Serverless/edge deployment — tidak bisa `spawn` editor
|
||||
|
||||
### 3. React sebagai UI Framework (Wajib untuk Multi-Fallback)
|
||||
|
||||
Strategi extraction source info bergantung pada React internals:
|
||||
1. `__reactProps$*` — React menyimpan props di DOM element
|
||||
2. `__reactFiber$*` — React fiber tree untuk walk-up
|
||||
3. DOM attribute — fallback universal
|
||||
|
||||
**Jika pakai framework lain** (Vue, Svelte, Solid):
|
||||
- Hanya strategi 3 (DOM attribute) yang berfungsi — tetap cukup
|
||||
- Hapus strategi 1 & 2 dari `getCodeInfoFromElement()`
|
||||
- Inject attributes tetap via Vite plugin (framework-agnostic)
|
||||
|
||||
### 4. Bun sebagai Runtime (Direkomendasikan, Bukan Wajib)
|
||||
|
||||
Bun memberikan API yang lebih clean:
|
||||
- `Bun.spawn()` — fire-and-forget tanpa import
|
||||
- `Bun.which()` — cek executable ada di PATH (mencegah uncatchable error)
|
||||
|
||||
**Jika pakai Node.js:**
|
||||
- `Bun.spawn()` → `child_process.spawn(editor, args, { detached: true, stdio: 'ignore' }).unref()`
|
||||
- `Bun.which()` → `const which = require('which'); which.sync(editor, { nothrow: true })`
|
||||
|
||||
### Ringkasan Syarat
|
||||
|
||||
| Syarat | Wajib? | Alternatif |
|
||||
|-------------------------------|----------|------------------------------------------------------|
|
||||
| Vite sebagai bundler | Ya | Framework berbasis Vite (Remix, Astro, dll) |
|
||||
| Server + frontend satu proses | Ya | Bisa diakali dengan proxy, tapi tambah kompleksitas |
|
||||
| React | Sebagian | Framework lain bisa, hanya fallback ke DOM attribute |
|
||||
| Bun runtime | Tidak | Node.js dengan `child_process` + `which` package |
|
||||
|
||||
## Arsitektur
|
||||
|
||||
```
|
||||
BUILD TIME (Vite Plugin):
|
||||
.tsx/.jsx file
|
||||
→ [inspectorPlugin enforce:'pre'] inject data-inspector-* attributes ke JSX
|
||||
→ [react() OXC] transform JSX ke createElement
|
||||
→ Browser menerima elemen dengan attributes
|
||||
|
||||
RUNTIME (Browser):
|
||||
Hotkey → aktifkan mode → hover elemen → baca attributes → klik
|
||||
→ POST /__open-in-editor {relativePath, line, column}
|
||||
|
||||
BACKEND (Elysia onRequest):
|
||||
/__open-in-editor → Bun.spawn([editor, '--goto', 'file:line:col'])
|
||||
→ Editor terbuka di lokasi tepat
|
||||
```
|
||||
|
||||
## Komponen yang Dibutuhkan
|
||||
|
||||
### 1. Vite Plugin — `inspectorPlugin()` (enforce: 'pre')
|
||||
|
||||
Inject `data-inspector-*` ke setiap JSX opening tag via regex.
|
||||
|
||||
**HARUS `enforce: 'pre'`** — kalau tidak, OXC transform JSX duluan dan regex tidak bisa menemukan `<Component`.
|
||||
|
||||
```typescript
|
||||
// Taruh di file vite config (misal: src/vite.ts atau vite.config.ts)
|
||||
import path from 'node:path'
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
function inspectorPlugin(): Plugin {
|
||||
const rootDir = process.cwd()
|
||||
|
||||
return {
|
||||
name: 'inspector-inject',
|
||||
enforce: 'pre',
|
||||
transform(code, id) {
|
||||
// Hanya .tsx/.jsx, skip node_modules
|
||||
if (!/\.[jt]sx(\?|$)/.test(id) || id.includes('node_modules')) return null
|
||||
if (!code.includes('<')) return null
|
||||
|
||||
const relativePath = path.relative(rootDir, id)
|
||||
let modified = false
|
||||
const lines = code.split('\n')
|
||||
const result: string[] = []
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i]
|
||||
// Match JSX opening tags: <Component atau <div
|
||||
// Skip TypeScript generics (Record<string>) via charBefore check
|
||||
const jsxPattern = /(<(?:[A-Z][a-zA-Z0-9.]*|[a-z][a-zA-Z0-9-]*))\b/g
|
||||
let match: RegExpExecArray | null = null
|
||||
|
||||
while ((match = jsxPattern.exec(line)) !== null) {
|
||||
// Skip jika karakter sebelum `<` adalah identifier char (TypeScript generic)
|
||||
const charBefore = match.index > 0 ? line[match.index - 1] : ''
|
||||
if (/[a-zA-Z0-9_$.]/.test(charBefore)) continue
|
||||
|
||||
const col = match.index + 1
|
||||
const attr = ` data-inspector-line="${i + 1}" data-inspector-column="${col}" data-inspector-relative-path="${relativePath}"`
|
||||
const insertPos = match.index + match[0].length
|
||||
line = line.slice(0, insertPos) + attr + line.slice(insertPos)
|
||||
modified = true
|
||||
jsxPattern.lastIndex += attr.length
|
||||
}
|
||||
|
||||
result.push(line)
|
||||
}
|
||||
|
||||
if (!modified) return null
|
||||
return result.join('\n')
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Mengapa regex, bukan Babel?**
|
||||
- `@vitejs/plugin-react` v6+ pakai OXC (Rust), bukan Babel
|
||||
- Config `babel: { plugins: [...] }` di plugin-react **DIABAIKAN**
|
||||
- Regex jalan sebelum OXC via `enforce: 'pre'`
|
||||
|
||||
**Gotcha: TypeScript generics**
|
||||
- `Record<string>` → karakter sebelum `<` adalah `d` (identifier) → SKIP
|
||||
- `<Button` → karakter sebelum `<` adalah space/newline → MATCH
|
||||
|
||||
### 2. Vite Plugin Order (KRITIS)
|
||||
|
||||
```typescript
|
||||
plugins: [
|
||||
// 1. Route generation (jika pakai TanStack Router)
|
||||
TanStackRouterVite({ ... }),
|
||||
|
||||
// 2. Inspector inject — HARUS sebelum react()
|
||||
inspectorPlugin(),
|
||||
|
||||
// 3. React OXC transform
|
||||
react(),
|
||||
|
||||
// 4. (Opsional) Dedupe React Refresh untuk middlewareMode
|
||||
dedupeRefreshPlugin(),
|
||||
]
|
||||
```
|
||||
|
||||
**Jika urutan salah (inspectorPlugin setelah react):**
|
||||
- OXC transform `<Button>` → `React.createElement(Button, ...)`
|
||||
- Regex tidak menemukan `<Button` → attributes TIDAK ter-inject
|
||||
- Fitur tidak berfungsi, tanpa error
|
||||
|
||||
### 3. DevInspector Component (Browser Runtime)
|
||||
|
||||
Komponen React yang handle hotkey, overlay, dan klik.
|
||||
|
||||
```tsx
|
||||
// src/frontend/DevInspector.tsx
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface CodeInfo {
|
||||
relativePath: string
|
||||
line: string
|
||||
column: string
|
||||
}
|
||||
|
||||
/** Baca data-inspector-* dari fiber props atau DOM attributes */
|
||||
function getCodeInfoFromElement(element: HTMLElement): CodeInfo | null {
|
||||
// Strategi 1: React internal props __reactProps$ (paling akurat)
|
||||
for (const key of Object.keys(element)) {
|
||||
if (key.startsWith('__reactProps$')) {
|
||||
const props = (element as any)[key]
|
||||
if (props?.['data-inspector-relative-path']) {
|
||||
return {
|
||||
relativePath: props['data-inspector-relative-path'],
|
||||
line: props['data-inspector-line'] || '1',
|
||||
column: props['data-inspector-column'] || '1',
|
||||
}
|
||||
}
|
||||
}
|
||||
// Strategi 2: Walk fiber tree __reactFiber$
|
||||
if (key.startsWith('__reactFiber$')) {
|
||||
const fiber = (element as any)[key]
|
||||
let f = fiber
|
||||
while (f) {
|
||||
const p = f.pendingProps || f.memoizedProps
|
||||
if (p?.['data-inspector-relative-path']) {
|
||||
return {
|
||||
relativePath: p['data-inspector-relative-path'],
|
||||
line: p['data-inspector-line'] || '1',
|
||||
column: p['data-inspector-column'] || '1',
|
||||
}
|
||||
}
|
||||
// Fallback: _debugSource (React < 19)
|
||||
const src = f._debugSource ?? f._debugOwner?._debugSource
|
||||
if (src?.fileName && src?.lineNumber) {
|
||||
return {
|
||||
relativePath: src.fileName,
|
||||
line: String(src.lineNumber),
|
||||
column: String(src.columnNumber ?? 1),
|
||||
}
|
||||
}
|
||||
f = f.return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategi 3: Fallback DOM attribute langsung
|
||||
const rp = element.getAttribute('data-inspector-relative-path')
|
||||
if (rp) {
|
||||
return {
|
||||
relativePath: rp,
|
||||
line: element.getAttribute('data-inspector-line') || '1',
|
||||
column: element.getAttribute('data-inspector-column') || '1',
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/** Walk up DOM tree sampai ketemu elemen yang punya source info */
|
||||
function findCodeInfo(target: HTMLElement): CodeInfo | null {
|
||||
let el: HTMLElement | null = target
|
||||
while (el) {
|
||||
const info = getCodeInfoFromElement(el)
|
||||
if (info) return info
|
||||
el = el.parentElement
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function openInEditor(info: CodeInfo) {
|
||||
fetch('/__open-in-editor', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
relativePath: info.relativePath,
|
||||
lineNumber: info.line,
|
||||
columnNumber: info.column,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export function DevInspector({ children }: { children: React.ReactNode }) {
|
||||
const [active, setActive] = useState(false)
|
||||
const overlayRef = useRef<HTMLDivElement | null>(null)
|
||||
const tooltipRef = useRef<HTMLDivElement | null>(null)
|
||||
const lastInfoRef = useRef<CodeInfo | null>(null)
|
||||
|
||||
const updateOverlay = useCallback((target: HTMLElement | null) => {
|
||||
const ov = overlayRef.current
|
||||
const tt = tooltipRef.current
|
||||
if (!ov || !tt) return
|
||||
|
||||
if (!target) {
|
||||
ov.style.display = 'none'
|
||||
tt.style.display = 'none'
|
||||
lastInfoRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
const info = findCodeInfo(target)
|
||||
if (!info) {
|
||||
ov.style.display = 'none'
|
||||
tt.style.display = 'none'
|
||||
lastInfoRef.current = null
|
||||
return
|
||||
}
|
||||
|
||||
lastInfoRef.current = info
|
||||
|
||||
const rect = target.getBoundingClientRect()
|
||||
ov.style.display = 'block'
|
||||
ov.style.top = `${rect.top + window.scrollY}px`
|
||||
ov.style.left = `${rect.left + window.scrollX}px`
|
||||
ov.style.width = `${rect.width}px`
|
||||
ov.style.height = `${rect.height}px`
|
||||
|
||||
tt.style.display = 'block'
|
||||
tt.textContent = `${info.relativePath}:${info.line}`
|
||||
const ttTop = rect.top + window.scrollY - 24
|
||||
tt.style.top = `${ttTop > 0 ? ttTop : rect.bottom + window.scrollY + 4}px`
|
||||
tt.style.left = `${rect.left + window.scrollX}px`
|
||||
}, [])
|
||||
|
||||
// Activate/deactivate event listeners
|
||||
useEffect(() => {
|
||||
if (!active) return
|
||||
|
||||
const onMouseOver = (e: MouseEvent) => updateOverlay(e.target as HTMLElement)
|
||||
|
||||
const onClick = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const info = lastInfoRef.current ?? findCodeInfo(e.target as HTMLElement)
|
||||
if (info) {
|
||||
const loc = `${info.relativePath}:${info.line}:${info.column}`
|
||||
console.log('[DevInspector] Open:', loc)
|
||||
navigator.clipboard.writeText(loc)
|
||||
openInEditor(info)
|
||||
}
|
||||
setActive(false)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setActive(false)
|
||||
}
|
||||
|
||||
document.addEventListener('mouseover', onMouseOver, true)
|
||||
document.addEventListener('click', onClick, true)
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
document.body.style.cursor = 'crosshair'
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseover', onMouseOver, true)
|
||||
document.removeEventListener('click', onClick, true)
|
||||
document.removeEventListener('keydown', onKeyDown)
|
||||
document.body.style.cursor = ''
|
||||
if (overlayRef.current) overlayRef.current.style.display = 'none'
|
||||
if (tooltipRef.current) tooltipRef.current.style.display = 'none'
|
||||
}
|
||||
}, [active, updateOverlay])
|
||||
|
||||
// Hotkey: Ctrl+Shift+Cmd+C (macOS) / Ctrl+Shift+Alt+C
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key.toLowerCase() === 'c' && e.ctrlKey && e.shiftKey && (e.metaKey || e.altKey)) {
|
||||
e.preventDefault()
|
||||
setActive((prev) => !prev)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
return () => document.removeEventListener('keydown', onKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<div
|
||||
ref={overlayRef}
|
||||
style={{
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
border: '2px solid #3b82f6',
|
||||
backgroundColor: 'rgba(59,130,246,0.1)',
|
||||
zIndex: 99999,
|
||||
transition: 'all 0.05s ease',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
style={{
|
||||
display: 'none',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: '#1e293b',
|
||||
color: '#e2e8f0',
|
||||
fontSize: '12px',
|
||||
fontFamily: 'monospace',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '3px',
|
||||
zIndex: 100000,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Backend Endpoint — `/__open-in-editor`
|
||||
|
||||
**HARUS ditangani di `onRequest` / sebelum middleware**, bukan sebagai route biasa. Kalau jadi route, akan kena auth middleware dan gagal.
|
||||
|
||||
```typescript
|
||||
// Di entry point server (src/index.tsx), dalam onRequest handler:
|
||||
|
||||
if (!isProduction && pathname === '/__open-in-editor' && request.method === 'POST') {
|
||||
const { relativePath, lineNumber, columnNumber } = (await request.json()) as {
|
||||
relativePath: string
|
||||
lineNumber: string
|
||||
columnNumber: string
|
||||
}
|
||||
const file = `${process.cwd()}/${relativePath}`
|
||||
const editor = process.env.REACT_EDITOR || 'code'
|
||||
const loc = `${file}:${lineNumber}:${columnNumber}`
|
||||
const args = editor === 'subl' ? [loc] : ['--goto', loc]
|
||||
const editorPath = Bun.which(editor)
|
||||
console.log(`[inspector] ${editor} → ${editorPath ?? 'NOT FOUND'} → ${loc}`)
|
||||
if (editorPath) {
|
||||
Bun.spawn([editor, ...args], { stdio: ['ignore', 'ignore', 'ignore'] })
|
||||
} else {
|
||||
console.error(`[inspector] Editor "${editor}" not found in PATH. Set REACT_EDITOR in .env`)
|
||||
}
|
||||
return new Response('ok')
|
||||
}
|
||||
```
|
||||
|
||||
**Penting — `Bun.which()` sebelum `Bun.spawn()`:**
|
||||
- `Bun.spawn()` throw native error yang TIDAK bisa di-catch jika executable tidak ada
|
||||
- `Bun.which()` return null dengan aman → cek dulu sebelum spawn
|
||||
|
||||
**Editor yang didukung:**
|
||||
|
||||
| REACT_EDITOR | Editor | Args |
|
||||
|------------------|--------------|--------------------------------|
|
||||
| `code` (default) | VS Code | `--goto file:line:col` |
|
||||
| `cursor` | Cursor | `--goto file:line:col` |
|
||||
| `windsurf` | Windsurf | `--goto file:line:col` |
|
||||
| `subl` | Sublime Text | `file:line:col` (tanpa --goto) |
|
||||
|
||||
### 5. Frontend Entry — Conditional Import (Zero Production Overhead)
|
||||
|
||||
```tsx
|
||||
// src/frontend.tsx (atau entry point React)
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
const InspectorWrapper = import.meta.env?.DEV
|
||||
? (await import('./frontend/DevInspector')).DevInspector
|
||||
: ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
|
||||
const app = (
|
||||
<InspectorWrapper>
|
||||
<App />
|
||||
</InspectorWrapper>
|
||||
)
|
||||
```
|
||||
|
||||
**Bagaimana zero overhead tercapai:**
|
||||
- `import.meta.env?.DEV` adalah compile-time constant
|
||||
- Production build: `false` → dynamic import TIDAK dieksekusi
|
||||
- Tree-shaking menghapus seluruh `DevInspector.tsx` dari bundle
|
||||
- Tidak ada runtime check, tidak ada dead code di bundle
|
||||
|
||||
### 6. (Opsional) Dedupe React Refresh — Workaround Vite middlewareMode
|
||||
|
||||
Jika pakai Vite dalam `middlewareMode` (seperti di Elysia/Express), `@vitejs/plugin-react` v6 bisa inject React Refresh footer dua kali → error "already declared".
|
||||
|
||||
```typescript
|
||||
function dedupeRefreshPlugin(): Plugin {
|
||||
return {
|
||||
name: 'dedupe-react-refresh',
|
||||
enforce: 'post',
|
||||
transform(code, id) {
|
||||
if (!/\.[jt]sx(\?|$)/.test(id) || id.includes('node_modules')) return null
|
||||
|
||||
const marker = 'import * as RefreshRuntime from "/@react-refresh"'
|
||||
const firstIdx = code.indexOf(marker)
|
||||
if (firstIdx === -1) return null
|
||||
|
||||
const secondIdx = code.indexOf(marker, firstIdx + marker.length)
|
||||
if (secondIdx === -1) return null
|
||||
|
||||
const sourcemapIdx = code.indexOf('\n//# sourceMappingURL=', secondIdx)
|
||||
const endIdx = sourcemapIdx !== -1 ? sourcemapIdx : code.length
|
||||
|
||||
const cleaned = code.slice(0, secondIdx) + code.slice(endIdx)
|
||||
return { code: cleaned, map: null }
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Langkah Implementasi di Project Baru
|
||||
|
||||
### Prasyarat
|
||||
- Runtime: Bun
|
||||
- Server: Elysia (atau framework lain dengan onRequest/beforeHandle)
|
||||
- Frontend: React + Vite
|
||||
- `@vitejs/plugin-react` (OXC)
|
||||
|
||||
### Step-by-step
|
||||
|
||||
1. **Buat `DevInspector.tsx`** — copy komponen dari Bagian 3 ke folder frontend
|
||||
2. **Tambah `inspectorPlugin()`** — copy fungsi dari Bagian 1 ke file vite config
|
||||
3. **Atur plugin order** — `inspectorPlugin()` SEBELUM `react()` (Bagian 2)
|
||||
4. **Tambah endpoint `/__open-in-editor`** — di `onRequest` handler (Bagian 4)
|
||||
5. **Wrap root app** — conditional import di entry point (Bagian 5)
|
||||
6. **Set env** — `REACT_EDITOR=code` (atau cursor/windsurf/subl) di `.env`
|
||||
7. **(Opsional)** Tambah `dedupeRefreshPlugin()` jika pakai Vite `middlewareMode`
|
||||
|
||||
### Checklist Verifikasi
|
||||
|
||||
- [ ] `inspectorPlugin` punya `enforce: 'pre'`
|
||||
- [ ] Plugin order: inspector → react (bukan sebaliknya)
|
||||
- [ ] Endpoint `/__open-in-editor` di LUAR middleware auth
|
||||
- [ ] `Bun.which(editor)` dipanggil SEBELUM `Bun.spawn()`
|
||||
- [ ] Conditional import pakai `import.meta.env?.DEV`
|
||||
- [ ] `REACT_EDITOR` di `.env` sesuai editor yang dipakai
|
||||
- [ ] Hotkey berfungsi: `Ctrl+Shift+Cmd+C` / `Ctrl+Shift+Alt+C`
|
||||
|
||||
## Gotcha & Pelajaran
|
||||
|
||||
| Masalah | Penyebab | Solusi |
|
||||
|----------------------------------|---------------------------------------------|-----------------------------------------------|
|
||||
| Attributes tidak ter-inject | Plugin order salah | `enforce: 'pre'`, taruh sebelum `react()` |
|
||||
| `Record<string>` ikut ter-inject | Regex match TypeScript generics | Cek `charBefore` — skip jika identifier char |
|
||||
| `Bun.spawn` crash | Editor tidak ada di PATH | Selalu `Bun.which()` dulu |
|
||||
| Hotkey tidak response | `e.key` return 'C' (uppercase) karena Shift | Pakai `e.key.toLowerCase()` |
|
||||
| React Refresh duplicate | Vite middlewareMode bug | `dedupeRefreshPlugin()` enforce: 'post' |
|
||||
| Endpoint kena auth middleware | Didaftarkan sebagai route biasa | Tangani di `onRequest` sebelum routing |
|
||||
| `_debugSource` undefined | React 19 menghapusnya | Multi-fallback: reactProps → fiber → DOM attr |
|
||||
|
||||
## Adaptasi untuk Framework Lain
|
||||
|
||||
### Express/Fastify (bukan Elysia)
|
||||
- Endpoint `/__open-in-editor`: gunakan middleware biasa SEBELUM auth
|
||||
- `Bun.spawn` → `child_process.spawn` jika pakai Node.js
|
||||
- `Bun.which` → `which` npm package jika pakai Node.js
|
||||
|
||||
### Next.js
|
||||
- Tidak perlu — Next.js punya built-in click-to-source
|
||||
- Tapi jika ingin custom: taruh endpoint di `middleware.ts`, plugin di `next.config.js`
|
||||
|
||||
### Remix/Tanstack Start (SSR)
|
||||
- Plugin tetap sama (Vite-based)
|
||||
- Endpoint perlu di server entry, bukan di route loader
|
||||
13
docker-entrypoint.sh
Normal file
13
docker-entrypoint.sh
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔄 Running database migrations..."
|
||||
cd /app
|
||||
bunx prisma migrate deploy || {
|
||||
echo "❌ Migration failed!"
|
||||
exit 1
|
||||
}
|
||||
echo "✅ Migrations completed successfully"
|
||||
|
||||
echo "🚀 Starting application..."
|
||||
exec bun start
|
||||
@@ -11,6 +11,11 @@ const compat = new FlatCompat({
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default eslintConfig;
|
||||
|
||||
99
gambar.ttx
Normal file
99
gambar.ttx
Normal file
@@ -0,0 +1,99 @@
|
||||
type DirItem = {
|
||||
type: "file" | "dir";
|
||||
name: string;
|
||||
path: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
// type FileDownloadResponse = {
|
||||
// url: string;
|
||||
// };
|
||||
|
||||
// const TOKEN = "20a19f4a04032215d50ce53292e6abdd38b9f806";
|
||||
// const REPO_ID = "8814bfe1-30d5-4e77-ab36-3122fa59a022";
|
||||
// const DIR_TARGET = "image";
|
||||
|
||||
// const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
|
||||
|
||||
const TOKEN = process.env.SEAFILE_TOKEN!;
|
||||
const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
||||
|
||||
// ⛔ PENTING: RELATIVE PATH (tanpa slash depan)
|
||||
const DIR_TARGET = "asset-web";
|
||||
|
||||
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
|
||||
|
||||
const headers = {
|
||||
Authorization: `Token ${TOKEN}`,
|
||||
};
|
||||
|
||||
/**
|
||||
* Ambil list file di directory
|
||||
*/
|
||||
async function getDirItems(): Promise<DirItem[]> {
|
||||
const res = await fetch(
|
||||
`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed get dir items: ${res.statusText}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil download URL file
|
||||
*/
|
||||
async function getDownloadUrl(filePath: string): Promise<string> {
|
||||
|
||||
|
||||
const res = await fetch(
|
||||
`${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}`,
|
||||
{ headers }
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed get file url: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil semua download URL dari target dir
|
||||
*/
|
||||
async function getAllDownloadUrls() {
|
||||
const items = await getDirItems();
|
||||
|
||||
const files = items.filter((item) => item.type === "file");
|
||||
|
||||
const results = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const filePath = `${DIR_TARGET}/${file.name}`;
|
||||
const url = await getDownloadUrl(filePath);
|
||||
return {
|
||||
name: file.name,
|
||||
path: filePath,
|
||||
downloadUrl: url,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// contoh eksekusi
|
||||
(async () => {
|
||||
try {
|
||||
console.log("ambil gambar")
|
||||
const urls = await getAllDownloadUrls();
|
||||
await Bun.write("list_image2.json", JSON.stringify(urls))
|
||||
console.log("selesai !")
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
serverExternalPackages: ['@elysiajs/static', 'elysia'],
|
||||
experimental: {},
|
||||
allowedDevOrigins: [
|
||||
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
||||
"http://localhost:3000", // akses lokal
|
||||
],
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -14,7 +20,6 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
60
package.json
60
package.json
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"name": "desa-darmasaba",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"prisma:seed": "bun run prisma/seed.ts"
|
||||
"test:api": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"test": "bun run test:api && bun run test:e2e",
|
||||
"gen:api": ""
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "bun run prisma/seed.ts"
|
||||
@@ -21,6 +23,7 @@
|
||||
"@elysiajs/static": "^1.3.0",
|
||||
"@elysiajs/stream": "^1.1.0",
|
||||
"@elysiajs/swagger": "^1.2.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@mantine/carousel": "^7.16.2",
|
||||
"@mantine/charts": "^7.17.1",
|
||||
"@mantine/core": "^7.17.4",
|
||||
@@ -28,9 +31,10 @@
|
||||
"@mantine/dropzone": "^8.1.1",
|
||||
"@mantine/form": "^8.1.0",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/modals": "^8.3.6",
|
||||
"@mantine/tiptap": "^7.17.4",
|
||||
"@paljs/types": "^8.1.0",
|
||||
"@prisma/client": "^6.3.1",
|
||||
"@prisma/client": "6.3.1",
|
||||
"@tabler/icons-react": "^3.30.0",
|
||||
"@tiptap/extension-highlight": "^2.11.7",
|
||||
"@tiptap/extension-link": "^2.11.7",
|
||||
@@ -41,61 +45,91 @@
|
||||
"@tiptap/pm": "^2.11.7",
|
||||
"@tiptap/react": "^2.11.7",
|
||||
"@tiptap/starter-kit": "^2.11.7",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/bun": "^1.2.2",
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/mime-types": "^3.0.1",
|
||||
"@types/nodemailer": "^7.0.2",
|
||||
"add": "^2.0.6",
|
||||
"adm-zip": "^0.5.16",
|
||||
"animate.css": "^4.1.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"bun": "^1.2.2",
|
||||
"chart.js": "^4.4.8",
|
||||
"classnames": "^2.5.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"colors": "^1.4.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dompurify": "^3.3.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"elysia": "^1.3.5",
|
||||
"embla-carousel-autoplay": "^8.5.2",
|
||||
"embla-carousel-react": "^7.1.0",
|
||||
"embla-carousel": "^8.6.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-react": "^8.6.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"form-data": "^4.0.2",
|
||||
"framer-motion": "^12.23.5",
|
||||
"framer-motion": "^12.38.0",
|
||||
"get-port": "^7.1.0",
|
||||
"iron-session": "^8.0.4",
|
||||
"jose": "^6.1.0",
|
||||
"jotai": "^2.12.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"list": "^2.0.19",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^3.0.2",
|
||||
"motion": "^12.4.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"next": "15.1.6",
|
||||
"next": "^15.5.2",
|
||||
"next-view-transitions": "^0.3.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
"nodemailer": "^7.0.10",
|
||||
"p-limit": "^6.2.0",
|
||||
"primeicons": "^7.0.0",
|
||||
"primereact": "^10.9.6",
|
||||
"prisma": "^6.3.1",
|
||||
"prisma": "6.3.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-exif-orientation-img": "^0.1.5",
|
||||
"react-international-phone": "^4.6.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-simple-toasts": "^6.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-zoom-pan-pinch": "^3.7.0",
|
||||
"readdirp": "^4.1.1",
|
||||
"recharts": "^2.15.3",
|
||||
"recharts": "^3.8.0",
|
||||
"sharp": "^0.34.3",
|
||||
"swr": "^2.3.2",
|
||||
"uuid": "^11.1.0",
|
||||
"valtio": "^2.1.3",
|
||||
"zlib": "^1.0.5",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@types/cli-progress": "^3.11.6",
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.6",
|
||||
"eslint-config-next": "15.5.12",
|
||||
"jsdom": "^28.0.0",
|
||||
"msw": "^2.12.9",
|
||||
"parcel": "^2.6.2",
|
||||
"playwright-mcp": "^0.0.19",
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# Page snapshot
|
||||
|
||||
```yaml
|
||||
- generic [active] [ref=e1]:
|
||||
- generic [ref=e2]:
|
||||
- generic [ref=e7]:
|
||||
- button "Darmasaba Logo" [ref=e8] [cursor=pointer]:
|
||||
- img "Darmasaba Logo" [ref=e10]
|
||||
- button "PPID" [ref=e11] [cursor=pointer]:
|
||||
- generic [ref=e13]: PPID
|
||||
- button "Desa" [ref=e14] [cursor=pointer]:
|
||||
- generic [ref=e16]: Desa
|
||||
- button "Kesehatan" [ref=e17] [cursor=pointer]:
|
||||
- generic [ref=e19]: Kesehatan
|
||||
- button "Keamanan" [ref=e20] [cursor=pointer]:
|
||||
- generic [ref=e22]: Keamanan
|
||||
- button "Ekonomi" [ref=e23] [cursor=pointer]:
|
||||
- generic [ref=e25]: Ekonomi
|
||||
- button "Inovasi" [ref=e26] [cursor=pointer]:
|
||||
- generic [ref=e28]: Inovasi
|
||||
- button "Lingkungan" [ref=e29] [cursor=pointer]:
|
||||
- generic [ref=e31]: Lingkungan
|
||||
- button "Pendidikan" [ref=e32] [cursor=pointer]:
|
||||
- generic [ref=e34]: Pendidikan
|
||||
- button "Musik" [ref=e35] [cursor=pointer]:
|
||||
- generic [ref=e37]: Musik
|
||||
- button [ref=e38] [cursor=pointer]:
|
||||
- img [ref=e40]
|
||||
- generic [ref=e46]:
|
||||
- generic [ref=e51]:
|
||||
- generic [ref=e52]:
|
||||
- generic [ref=e53]:
|
||||
- img "Logo Darmasaba" [ref=e55]
|
||||
- img "Logo Pudak" [ref=e57]
|
||||
- generic [ref=e63]:
|
||||
- generic [ref=e65]:
|
||||
- generic [ref=e66]:
|
||||
- img [ref=e67]
|
||||
- paragraph [ref=e71]: Jam Operasional
|
||||
- generic [ref=e72]:
|
||||
- generic [ref=e74]: Buka
|
||||
- paragraph [ref=e75]: 07:30 - 15:30
|
||||
- generic [ref=e77]:
|
||||
- generic [ref=e78]:
|
||||
- img [ref=e79]
|
||||
- paragraph [ref=e82]: Hari Ini
|
||||
- generic [ref=e83]:
|
||||
- paragraph [ref=e84]: Status Kantor
|
||||
- paragraph [ref=e85]: Sedang Beroperasi
|
||||
- paragraph [ref=e95]: Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. Semua lebih mudah dengan fitur interaktif yang kami sediakan.
|
||||
- generic [ref=e102]:
|
||||
- generic [ref=e103]: Browser Anda tidak mendukung video.
|
||||
- generic [ref=e106]:
|
||||
- heading "Penghargaan Desa" [level=2] [ref=e107]
|
||||
- paragraph [ref=e110]: Sedang memuat data penghargaan...
|
||||
- button "Lihat semua penghargaan" [ref=e111] [cursor=pointer]:
|
||||
- generic [ref=e112]:
|
||||
- paragraph [ref=e114]: Lihat Semua Penghargaan
|
||||
- img [ref=e116]
|
||||
- generic [ref=e119]:
|
||||
- generic [ref=e121]:
|
||||
- heading "Layanan" [level=1] [ref=e122]
|
||||
- paragraph [ref=e123]: Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!
|
||||
- link "Detail" [ref=e125] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/layanan
|
||||
- generic [ref=e127]: Detail
|
||||
- separator [ref=e129]
|
||||
- generic [ref=e130]:
|
||||
- generic [ref=e131]:
|
||||
- paragraph [ref=e132]: Potensi Desa
|
||||
- paragraph [ref=e133]: Jelajahi berbagai potensi dan peluang yang dimiliki desa. Fitur ini membantu warga maupun pemerintah desa dalam merencanakan dan mengembangkan program berbasis kekuatan lokal.
|
||||
- paragraph [ref=e136]: Sedang memuat potensi desa...
|
||||
- button "Lihat Semua Potensi" [ref=e139] [cursor=pointer]:
|
||||
- generic [ref=e140]:
|
||||
- generic [ref=e141]: Lihat Semua Potensi
|
||||
- img [ref=e143]
|
||||
- separator [ref=e146]
|
||||
- generic [ref=e147]:
|
||||
- generic [ref=e148]:
|
||||
- paragraph [ref=e150]: Desa Anti Korupsi
|
||||
- paragraph [ref=e151]: Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola secara terbuka dengan melibatkan warga dalam pengawasan anggaran, sehingga digunakan tepat sasaran dan sesuai kebutuhan masyarakat.
|
||||
- link "Selengkapnya" [ref=e153] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa-anti-korupsi/detail
|
||||
- generic [ref=e155]: Selengkapnya
|
||||
- paragraph [ref=e158]: Memuat Data...
|
||||
- generic [ref=e166]:
|
||||
- heading "SDGs Desa" [level=1] [ref=e168]
|
||||
- paragraph [ref=e169]: SDGs Desa adalah upaya desa untuk menciptakan pembangunan yang maju, inklusif, dan berkelanjutan melalui 17 tujuan mulai dari pengentasan kemiskinan, pendidikan, kesehatan, hingga pelestarian lingkungan.
|
||||
- generic [ref=e170]:
|
||||
- generic [ref=e171]:
|
||||
- img [ref=e172]
|
||||
- paragraph [ref=e175]: Data SDGs Desa belum tersedia
|
||||
- link "Jelajahi Semua Tujuan SDGs Desa" [ref=e177] [cursor=pointer]:
|
||||
- /url: /darmasaba/sdgs-desa
|
||||
- paragraph [ref=e180]: Jelajahi Semua Tujuan SDGs Desa
|
||||
- generic [ref=e181]:
|
||||
- generic [ref=e183]:
|
||||
- heading "APBDes" [level=1] [ref=e184]
|
||||
- paragraph [ref=e185]: Transparansi APBDes Darmasaba adalah langkah nyata menuju tata kelola desa yang bersih, terbuka, dan bertanggung jawab.
|
||||
- link "Lihat Semua Data" [ref=e187] [cursor=pointer]:
|
||||
- /url: /darmasaba/apbdes
|
||||
- generic [ref=e189]: Lihat Semua Data
|
||||
- generic [ref=e191]:
|
||||
- paragraph [ref=e193]: Pilih Tahun APBDes
|
||||
- generic [ref=e194]:
|
||||
- textbox "Pilih Tahun APBDes" [ref=e195]:
|
||||
- /placeholder: Pilih tahun
|
||||
- generic:
|
||||
- img
|
||||
- paragraph [ref=e197]: Tidak ada data APBDes untuk tahun yang dipilih.
|
||||
- generic [ref=e202]:
|
||||
- heading "Prestasi Desa" [level=1] [ref=e203]
|
||||
- paragraph [ref=e204]: Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama.
|
||||
- link "Lihat Semua Prestasi" [ref=e205] [cursor=pointer]:
|
||||
- /url: /darmasaba/prestasi-desa
|
||||
- generic [ref=e207]: Lihat Semua Prestasi
|
||||
- button [ref=e211] [cursor=pointer]:
|
||||
- img [ref=e214]
|
||||
- button [ref=e219] [cursor=pointer]:
|
||||
- img [ref=e221]
|
||||
- generic [ref=e225]:
|
||||
- contentinfo [ref=e228]:
|
||||
- generic [ref=e230]:
|
||||
- generic [ref=e231]:
|
||||
- heading "Komitmen Layanan Kami" [level=2] [ref=e232]
|
||||
- generic [ref=e233]:
|
||||
- generic [ref=e234]:
|
||||
- paragraph [ref=e235]: "1. Transparansi:"
|
||||
- paragraph [ref=e236]: Pengelolaan dana desa dilakukan secara terbuka agar masyarakat dapat memahami dan memantau penggunaan anggaran.
|
||||
- generic [ref=e237]:
|
||||
- paragraph [ref=e238]: "2. Profesionalisme:"
|
||||
- paragraph [ref=e239]: Layanan desa diberikan secara cepat, adil, dan profesional demi kepuasan masyarakat.
|
||||
- generic [ref=e240]:
|
||||
- paragraph [ref=e241]: "3. Partisipasi:"
|
||||
- paragraph [ref=e242]: Masyarakat dilibatkan aktif dalam pengambilan keputusan demi pembangunan desa yang berhasil.
|
||||
- generic [ref=e243]:
|
||||
- paragraph [ref=e244]: "4. Inovasi:"
|
||||
- paragraph [ref=e245]: Kami terus berinovasi, termasuk melalui teknologi, agar layanan semakin mudah diakses.
|
||||
- generic [ref=e246]:
|
||||
- paragraph [ref=e247]: "5. Keadilan:"
|
||||
- paragraph [ref=e248]: Kebijakan dan program disusun untuk memberi manfaat yang merata bagi seluruh warga.
|
||||
- generic [ref=e249]:
|
||||
- paragraph [ref=e250]: "6. Pemberdayaan:"
|
||||
- paragraph [ref=e251]: Masyarakat didukung melalui pelatihan, pendampingan, dan pengembangan usaha lokal.
|
||||
- generic [ref=e252]:
|
||||
- paragraph [ref=e253]: "7. Ramah Lingkungan:"
|
||||
- paragraph [ref=e254]: Seluruh kegiatan pembangunan memperhatikan keberlanjutan demi menjaga alam dan kesehatan warga.
|
||||
- separator [ref=e255]
|
||||
- generic [ref=e256]:
|
||||
- heading "Visi Kami" [level=2] [ref=e257]
|
||||
- paragraph [ref=e258]: Dengan visi ini, kami berkomitmen menjadikan desa sebagai tempat yang aman, sejahtera, dan nyaman bagi seluruh warga.
|
||||
- paragraph [ref=e259]: Kami percaya kemajuan dimulai dari kerja sama antara pemerintah desa dan masyarakat, didukung tata kelola yang baik demi kepentingan bersama. Saran maupun keluhan dapat disampaikan melalui kontak di bawah ini.
|
||||
- generic [ref=e260]:
|
||||
- paragraph [ref=e261]: "\"Desa Kuat, Warga Sejahtera!\""
|
||||
- button "Logo Desa" [ref=e262] [cursor=pointer]:
|
||||
- generic [ref=e263]:
|
||||
- img "Logo Desa"
|
||||
- generic [ref=e265]:
|
||||
- generic [ref=e267]:
|
||||
- paragraph [ref=e268]: Tentang Darmasaba
|
||||
- paragraph [ref=e269]: Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
|
||||
- generic [ref=e270]:
|
||||
- link [ref=e271] [cursor=pointer]:
|
||||
- /url: https://www.facebook.com/DarmasabaDesaku
|
||||
- img [ref=e273]
|
||||
- link [ref=e275] [cursor=pointer]:
|
||||
- /url: https://www.instagram.com/ddarmasaba/
|
||||
- img [ref=e277]
|
||||
- link [ref=e280] [cursor=pointer]:
|
||||
- /url: https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg
|
||||
- img [ref=e282]
|
||||
- link [ref=e285] [cursor=pointer]:
|
||||
- /url: https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc
|
||||
- img [ref=e287]
|
||||
- generic [ref=e290]:
|
||||
- paragraph [ref=e291]: Layanan Desa
|
||||
- link "Administrasi Kependudukan" [ref=e292] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/layanan/
|
||||
- link "Layanan Sosial" [ref=e293] [cursor=pointer]:
|
||||
- /url: /darmasaba/ekonomi/program-kemiskinan
|
||||
- link "Pengaduan Masyarakat" [ref=e294] [cursor=pointer]:
|
||||
- /url: /darmasaba/keamanan/laporan-publik
|
||||
- link "Informasi Publik" [ref=e295] [cursor=pointer]:
|
||||
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
|
||||
- generic [ref=e297]:
|
||||
- paragraph [ref=e298]: Tautan Penting
|
||||
- link "Portal Badung" [ref=e299] [cursor=pointer]:
|
||||
- /url: /darmasaba/desa/berita/semua
|
||||
- link "E-Government" [ref=e300] [cursor=pointer]:
|
||||
- /url: /darmasaba/inovasi/desa-digital-smart-village
|
||||
- link "Transparansi" [ref=e301] [cursor=pointer]:
|
||||
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
|
||||
- generic [ref=e303]:
|
||||
- paragraph [ref=e304]: Berlangganan Info
|
||||
- paragraph [ref=e305]: Dapatkan kabar terbaru tentang program dan kegiatan desa langsung ke email Anda.
|
||||
- generic [ref=e306]:
|
||||
- generic [ref=e308]:
|
||||
- textbox "Masukkan email Anda" [ref=e309]
|
||||
- img [ref=e311]
|
||||
- button "Daftar" [ref=e314] [cursor=pointer]:
|
||||
- generic [ref=e316]: Daftar
|
||||
- separator [ref=e317]
|
||||
- paragraph [ref=e318]: © 2025 Desa Darmasaba. Hak cipta dilindungi.
|
||||
- region "Notifications Alt+T"
|
||||
- button "Open Next.js Dev Tools" [ref=e324] [cursor=pointer]:
|
||||
- img [ref=e325]
|
||||
- alert [ref=e328]
|
||||
```
|
||||
85
playwright-report/index.html
Normal file
85
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
25
playwright.config.ts
Normal file
25
playwright.config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './__tests__/e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: 'html',
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'bun run dev',
|
||||
url: 'http://localhost:3000',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,15 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '36em',
|
||||
'mantine-breakpoint-sm': '48em',
|
||||
'mantine-breakpoint-md': '62em',
|
||||
'mantine-breakpoint-lg': '75em',
|
||||
'mantine-breakpoint-xl': '88em',
|
||||
},
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
/* Mobile first */
|
||||
'mantine-breakpoint-xs': '30em', // 480px → mobile kecil–normal
|
||||
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
|
||||
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
|
||||
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
|
||||
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
57
prisma/_seeder_list/core/seed_app_menu.ts
Normal file
57
prisma/_seeder_list/core/seed_app_menu.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const appMenuJson = loadJsonData("core/app-menu.json");
|
||||
const appMenuChildJson = loadJsonData("core/app-menu-child.json");
|
||||
|
||||
export async function seedAppMenu() {
|
||||
console.log("🔄 Seeding AppMenu...");
|
||||
|
||||
for (const item of appMenuJson) {
|
||||
await prisma.appMenu.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ AppMenu seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 AppMenu seed selesai");
|
||||
}
|
||||
|
||||
export async function seedAppMenuChild() {
|
||||
console.log("🔄 Seeding AppMenuChild...");
|
||||
|
||||
for (const item of appMenuChildJson) {
|
||||
await prisma.appMenuChild.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
appMenuId: item.appMenuId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
link: item.link,
|
||||
isActive: item.isActive,
|
||||
appMenuId: item.appMenuId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ AppMenuChild seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 AppMenuChild seed selesai");
|
||||
}
|
||||
69
prisma/_seeder_list/core/seed_core.ts
Normal file
69
prisma/_seeder_list/core/seed_core.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const layananJson = loadJsonData("core/layanan.json");
|
||||
const potensiJson = loadJsonData("core/potensi.json");
|
||||
const landingPageLayananJson = loadJsonData("core/landingpage-layanan.json");
|
||||
|
||||
export async function seedLayananCore() {
|
||||
console.log("🔄 Seeding Layanan...");
|
||||
|
||||
for (const item of layananJson) {
|
||||
await prisma.layanan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Layanan seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Layanan seed selesai");
|
||||
}
|
||||
|
||||
export async function seedPotensiCore() {
|
||||
console.log("🔄 Seeding Potensi...");
|
||||
|
||||
for (const item of potensiJson) {
|
||||
await prisma.potensi.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Potensi seeded: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Potensi seed selesai");
|
||||
}
|
||||
|
||||
export async function seedLandingPageLayanan() {
|
||||
console.log("🔄 Seeding LandingPage_Layanan...");
|
||||
|
||||
for (const item of landingPageLayananJson) {
|
||||
await prisma.landingPage_Layanan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
deksripsi: item.deksripsi,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
deksripsi: item.deksripsi,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ LandingPage_Layanan seeded: ${item.id}`);
|
||||
}
|
||||
|
||||
console.log("🎉 LandingPage_Layanan seed selesai");
|
||||
}
|
||||
96
prisma/_seeder_list/desa/berita/seed_berita.ts
Normal file
96
prisma/_seeder_list/desa/berita/seed_berita.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const kategoriBerita = loadJsonData("desa/berita/kategori-berita.json");
|
||||
const beritaJson = loadJsonData("desa/berita/berita.json");
|
||||
|
||||
export async function seedBerita() {
|
||||
// ================== SUBMENU BERITA ========================
|
||||
console.log("🔄 Seeding Kategori Berita...");
|
||||
for (const k of kategoriBerita) {
|
||||
await prisma.kategoriBerita.upsert({
|
||||
where: {
|
||||
name: k.name, // ✅ cocok dengan @unique
|
||||
},
|
||||
update: {
|
||||
name: k.name,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
id: k.id, // ✅ id tetap bisa disimpan
|
||||
name: k.name,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("kategori berita success ...");
|
||||
|
||||
console.log("🔄 Seeding Berita...");
|
||||
|
||||
// Build a map of valid kategori IDs
|
||||
const validKategoriIds = new Set<string>();
|
||||
const kategoriList = await prisma.kategoriBerita.findMany({
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
kategoriList.forEach((k) => validKategoriIds.add(k.id));
|
||||
|
||||
console.log(`📋 Found ${validKategoriIds.size} valid kategori IDs in database`);
|
||||
|
||||
for (const b of beritaJson) {
|
||||
// Validate kategoriBeritaId exists
|
||||
if (!b.kategoriBeritaId || !validKategoriIds.has(b.kategoriBeritaId)) {
|
||||
console.warn(
|
||||
`⚠️ Skipping berita "${b.judul}": Invalid kategoriBeritaId "${b.kategoriBeritaId}"`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (b.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: b.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for berita "${b.judul}": ${b.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.berita.upsert({
|
||||
where: { id: b.id },
|
||||
update: {
|
||||
judul: b.judul,
|
||||
deskripsi: b.deskripsi,
|
||||
content: b.content,
|
||||
kategoriBeritaId: b.kategoriBeritaId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: b.id,
|
||||
judul: b.judul,
|
||||
deskripsi: b.deskripsi,
|
||||
content: b.content,
|
||||
kategoriBeritaId: b.kategoriBeritaId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Berita seeded: ${b.judul}`);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`❌ Failed to seed berita "${b.judul}": ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🎉 Berita seed selesai");
|
||||
}
|
||||
|
||||
42
prisma/_seeder_list/desa/gallery/foto/seed_foto.ts
Normal file
42
prisma/_seeder_list/desa/gallery/foto/seed_foto.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../../load-json";
|
||||
|
||||
const foto = loadJsonData("desa/gallery/foto/foto.json");
|
||||
|
||||
export async function seedFoto() {
|
||||
console.log("🔄 Seeding Foto...");
|
||||
for (const f of foto) {
|
||||
let imagesId: string | null = null;
|
||||
|
||||
if (f.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: f.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for foto "${f.name}": ${f.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imagesId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.galleryFoto.upsert({
|
||||
where: { id: f.id },
|
||||
update: {
|
||||
name: f.name,
|
||||
deskripsi: f.deskripsi,
|
||||
imagesId,
|
||||
},
|
||||
create: {
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
deskripsi: f.deskripsi,
|
||||
imagesId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Foto seeding completed");
|
||||
}
|
||||
27
prisma/_seeder_list/desa/gallery/video/seed_video.ts
Normal file
27
prisma/_seeder_list/desa/gallery/video/seed_video.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../../load-json";
|
||||
|
||||
const galleryVideo = loadJsonData("desa/gallery/video/video.json");
|
||||
|
||||
export async function seedVideo() {
|
||||
console.log("🔄 Seeding Gallery Video...");
|
||||
for (const v of galleryVideo) {
|
||||
await prisma.galleryVideo.upsert({
|
||||
where: {
|
||||
id: v.id,
|
||||
},
|
||||
update: {
|
||||
name: v.judul,
|
||||
deskripsi: v.deskripsi,
|
||||
linkVideo: v.linkVideo,
|
||||
},
|
||||
create: {
|
||||
name: v.judul,
|
||||
deskripsi: v.deskripsi,
|
||||
linkVideo: v.linkVideo,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("gallery video success ...");
|
||||
}
|
||||
130
prisma/_seeder_list/desa/layanan/seed_layanan.ts
Normal file
130
prisma/_seeder_list/desa/layanan/seed_layanan.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const pelayananSuratKeterangan = loadJsonData("desa/layanan/pelayananSuratKeterangan.json");
|
||||
const pelayananTelunjukSaktiDesa = loadJsonData("desa/layanan/pelayananTelunjukSaktiDesa.json");
|
||||
const pelayananPerizinanBerusaha = loadJsonData("desa/layanan/pelayananPerizinanBerusaha.json");
|
||||
const pelayananPendudukNonPermanen = loadJsonData("desa/layanan/pelayananPendudukNonPermanen.json");
|
||||
|
||||
export async function seedLayanan() {
|
||||
console.log("🔄 Seeding Pelayanan Surat Keterangan...");
|
||||
|
||||
for (const p of pelayananSuratKeterangan) {
|
||||
const existing = await prisma.pelayananSuratKeterangan.findUnique({
|
||||
where: { id: p.id },
|
||||
select: { imageId: true, image2Id: true }, // 📌 tambahkan image2Id
|
||||
});
|
||||
|
||||
// 1️⃣ Handle imageId
|
||||
let imageId = existing?.imageId ?? null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pelayanan surat keterangan 1 "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Handle image2Id
|
||||
let image2Id = existing?.image2Id ?? null;
|
||||
|
||||
if (p.image2Name) {
|
||||
const image2 = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.image2Name },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image2) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pelayanan surat keterangan 2 "${p.name}": ${p.image2Name}`,
|
||||
);
|
||||
} else {
|
||||
image2Id = image2.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ Upsert dengan kedua image
|
||||
await prisma.pelayananSuratKeterangan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
image2Id, // 📌 tambahkan ini
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
image2Id, // 📌 tambahkan ini
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pelayanan Surat Keterangan success...");
|
||||
|
||||
for (const p of pelayananTelunjukSaktiDesa) {
|
||||
await prisma.pelayananTelunjukSaktiDesa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
link: p.link,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
link: p.link,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pelayanan telunjuk sakti desa success ...");
|
||||
|
||||
for (const l of pelayananPerizinanBerusaha) {
|
||||
await prisma.pelayananPerizinanBerusaha.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
link: l.link,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
link: l.link,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("pelayanan perizinan berusaha success ...");
|
||||
|
||||
for (const l of pelayananPendudukNonPermanen) {
|
||||
await prisma.pelayananPendudukNonPermanen.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
name: l.name,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pelayanan penduduk non permanen success ...");
|
||||
}
|
||||
57
prisma/_seeder_list/desa/musik-desa/seed_musik_desa.ts
Normal file
57
prisma/_seeder_list/desa/musik-desa/seed_musik_desa.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const musikJson = loadJsonData("desa/musik-desa/musik-desa.json");
|
||||
|
||||
export async function seedMusikDesa() {
|
||||
console.log("Seeding Musik Desa...");
|
||||
|
||||
for (const item of musikJson) {
|
||||
let audioFileId: string | null = null;
|
||||
let coverImageId: string | null = null;
|
||||
|
||||
if (item.audioFileName) {
|
||||
const audio = await prisma.fileStorage.findUnique({
|
||||
where: { name: item.audioFileName },
|
||||
select: { id: true },
|
||||
});
|
||||
if (audio) audioFileId = audio.id;
|
||||
}
|
||||
|
||||
if (item.coverImageName) {
|
||||
const cover = await prisma.fileStorage.findUnique({
|
||||
where: { name: item.coverImageName },
|
||||
select: { id: true },
|
||||
});
|
||||
if (cover) coverImageId = cover.id;
|
||||
}
|
||||
|
||||
await prisma.musikDesa.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
judul: item.judul,
|
||||
artis: item.artis,
|
||||
deskripsi: item.deskripsi,
|
||||
durasi: item.durasi,
|
||||
audioFileId,
|
||||
coverImageId,
|
||||
genre: item.genre,
|
||||
tahunRilis: item.tahunRilis,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
judul: item.judul,
|
||||
artis: item.artis,
|
||||
deskripsi: item.deskripsi,
|
||||
durasi: item.durasi,
|
||||
audioFileId,
|
||||
coverImageId,
|
||||
genre: item.genre,
|
||||
tahunRilis: item.tahunRilis,
|
||||
},
|
||||
});
|
||||
console.log(` Musik: ${item.judul} - ${item.artis}`);
|
||||
}
|
||||
|
||||
console.log("Musik Desa seed selesai");
|
||||
}
|
||||
46
prisma/_seeder_list/desa/penghargaan/penghargaan.ts
Normal file
46
prisma/_seeder_list/desa/penghargaan/penghargaan.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const penghargaan = loadJsonData("desa/penghargaan/penghargaan.json");
|
||||
|
||||
export async function seedPenghargaan() {
|
||||
console.log("🔄 Seeding Penghargaan...");
|
||||
for (const m of penghargaan) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for penghargaan "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.penghargaan.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
juara: m.juara,
|
||||
deskripsi: m.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
juara: m.juara,
|
||||
deskripsi: m.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("penghargaan success ...");
|
||||
}
|
||||
|
||||
45
prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts
Normal file
45
prisma/_seeder_list/desa/pengumuman/seed_pengumuman.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
import { safeSeedUnique } from "../../../safeseedUnique";
|
||||
|
||||
const kategoriPengumuman = loadJsonData("desa/pengumuman/kategori-pengumuman.json");
|
||||
const pengumuman = loadJsonData("desa/pengumuman/pengumuman.json");
|
||||
|
||||
export async function seedPengumuman() {
|
||||
console.log("🔄 Seeding Kategori Pengumuman...");
|
||||
for (const c of kategoriPengumuman) {
|
||||
await safeSeedUnique(
|
||||
"categoryPengumuman",
|
||||
{ name: c.name }, // ✅ where clause
|
||||
{
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
console.log("kategori pengumuman success ...");
|
||||
|
||||
console.log("🔄 Seeding Pengumuman...");
|
||||
for (const p of pengumuman) {
|
||||
await prisma.pengumuman.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
content: p.content,
|
||||
categoryPengumumanId: p.categoryPengumumanId,
|
||||
},
|
||||
create: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
content: p.content,
|
||||
categoryPengumumanId: p.categoryPengumumanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("pengumuman success ...");
|
||||
}
|
||||
66
prisma/_seeder_list/desa/potensi/seed_potensi.ts
Normal file
66
prisma/_seeder_list/desa/potensi/seed_potensi.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const kategoriPotensi = loadJsonData("desa/potensi/kategori-potensi.json");
|
||||
const potensiDesa = loadJsonData("desa/potensi/potensi-desa.json");
|
||||
|
||||
export async function seedPotensi() {
|
||||
console.log("🔄Seeding Kategori Potensi Desa ...");
|
||||
for (const c of kategoriPotensi) {
|
||||
await prisma.kategoriPotensi.upsert({
|
||||
where: {
|
||||
id: c.id,
|
||||
},
|
||||
update: {
|
||||
nama: c.nama,
|
||||
},
|
||||
create: {
|
||||
id: c.id,
|
||||
nama: c.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("kategori Potensi success ...");
|
||||
|
||||
console.log("🔄 Seeding Potensi Desa...");
|
||||
for (const m of potensiDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for potensi desa "${m.name}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.potensiDesa.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
name: m.name,
|
||||
deskripsi: m.deskripsi,
|
||||
content: m.content,
|
||||
kategoriId: m.kategoriId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
deskripsi: m.deskripsi,
|
||||
content: m.content,
|
||||
kategoriId: m.kategoriId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("potensi desa success ...");
|
||||
}
|
||||
170
prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts
Normal file
170
prisma/_seeder_list/desa/profile-desa/seed_profile_desa.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const lambangDesa = loadJsonData("desa/profile/lambang_desa.json");
|
||||
const maskotDesa = loadJsonData("desa/profile/maskot_desa.json");
|
||||
const profilePerbekel = loadJsonData("desa/profile/profil_perbekel.json");
|
||||
const profileDesaImage = loadJsonData("desa/profile/profileDesaImage.json");
|
||||
const sejarahDesa = loadJsonData("desa/profile/sejarah_desa.json");
|
||||
const visiMisiDesa = loadJsonData("desa/profile/visi_misi_desa.json");
|
||||
|
||||
export async function seedProfileDesa() {
|
||||
// =========== SEJARAH DESA ===========
|
||||
for (const l of sejarahDesa) {
|
||||
await prisma.sejarahDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("sejarah desa success ...");
|
||||
|
||||
// =========== VISI MISI DESA ===========
|
||||
for (const l of visiMisiDesa) {
|
||||
await prisma.visiMisiDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
visi: l.visi,
|
||||
misi: l.misi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
visi: l.visi,
|
||||
misi: l.misi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("visi misi desa success ...");
|
||||
|
||||
// =========== MASKOT DESA ===========
|
||||
for (const l of maskotDesa) {
|
||||
await prisma.maskotDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("maskot desa success ...");
|
||||
|
||||
console.log("🔄 Seeding Profile Desa Image...");
|
||||
for (const m of profileDesaImage) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for profile desa image "${m.label}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.profileDesaImage.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
label: m.label,
|
||||
maskotDesaId: m.maskotDesaId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
maskotDesaId: m.maskotDesaId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("profile desa image success ...");
|
||||
|
||||
// =========== LAMBANG DESA ===========
|
||||
for (const l of lambangDesa) {
|
||||
await prisma.lambangDesa.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
deskripsi: l.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("lambang desa success ...");
|
||||
|
||||
// =========== PROFILE PERBEKEL PROFILE DESA ===========
|
||||
console.log("🔄 Seeding Profile Perbekel...");
|
||||
for (const m of profilePerbekel) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (m.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: m.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for profile perbekel "${m.biodata}": ${m.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.profilPerbekel.upsert({
|
||||
where: { id: m.id },
|
||||
update: {
|
||||
biodata: m.biodata,
|
||||
pengalaman: m.pengalaman,
|
||||
pengalamanOrganisasi: m.pengalamanOrganisasi,
|
||||
programUnggulan: m.programUnggulan,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: m.id,
|
||||
biodata: m.biodata,
|
||||
pengalaman: m.pengalaman,
|
||||
pengalamanOrganisasi: m.pengalamanOrganisasi,
|
||||
programUnggulan: m.programUnggulan,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("profile perbekel desa success ...");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const perbekelDariMasaKeMasa = loadJsonData("desa/profile/profile-perbekel-lalu.json");
|
||||
|
||||
export async function seedProfilePerbekel() {
|
||||
console.log("🔄 Seeding Perbekel Dari Masa Ke Masa...");
|
||||
for (const p of perbekelDariMasaKeMasa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for Perbekel Dari Masa Ke Masa "${p.nama}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.perbekelDariMasaKeMasa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
nama: p.nama,
|
||||
periode: p.periode,
|
||||
daerah: p.daerah,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
nama: p.nama,
|
||||
periode: p.periode,
|
||||
daerah: p.daerah,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pejabat Desa seeding completed");
|
||||
}
|
||||
45
prisma/_seeder_list/ekonomi/seed_apbdes.ts
Normal file
45
prisma/_seeder_list/ekonomi/seed_apbdes.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const apbdesJson = loadJsonData("ekonomi/apbdes/apbdes.json");
|
||||
|
||||
export async function seedAPBDes() {
|
||||
console.log("Seeding APBDes...");
|
||||
|
||||
for (const item of apbdesJson) {
|
||||
let imageId: string | null = null;
|
||||
let fileId: string | null = null;
|
||||
|
||||
if (item.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: item.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
if (image) imageId = image.id;
|
||||
}
|
||||
|
||||
await prisma.aPBDes.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
tahun: item.tahun,
|
||||
name: item.name,
|
||||
deskripsi: item.deskripsi,
|
||||
jumlah: item.jumlah,
|
||||
imageId,
|
||||
fileId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
tahun: item.tahun,
|
||||
name: item.name,
|
||||
deskripsi: item.deskripsi,
|
||||
jumlah: item.jumlah,
|
||||
imageId,
|
||||
fileId,
|
||||
},
|
||||
});
|
||||
console.log(` APBDes: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("APBDes seed selesai");
|
||||
}
|
||||
63
prisma/_seeder_list/ekonomi/seed_apbdes_item.ts
Normal file
63
prisma/_seeder_list/ekonomi/seed_apbdes_item.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const itemsJson = loadJsonData("ekonomi/apbdes/apbdes-items.json");
|
||||
const realisasiJson = loadJsonData("ekonomi/apbdes/realisasi-items.json");
|
||||
|
||||
export async function seedAPBDesItem() {
|
||||
console.log("Seeding APBDes Items...");
|
||||
|
||||
// Seed items first (sorted by level to ensure parents exist)
|
||||
const sortedItems = [...itemsJson].sort((a, b) => a.level - b.level);
|
||||
|
||||
for (const item of sortedItems) {
|
||||
await prisma.aPBDesItem.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
kode: item.kode,
|
||||
uraian: item.uraian,
|
||||
anggaran: item.anggaran,
|
||||
tipe: item.tipe,
|
||||
level: item.level,
|
||||
parentId: item.parentId,
|
||||
apbdesId: item.apbdesId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
kode: item.kode,
|
||||
uraian: item.uraian,
|
||||
anggaran: item.anggaran,
|
||||
tipe: item.tipe,
|
||||
level: item.level,
|
||||
parentId: item.parentId,
|
||||
apbdesId: item.apbdesId,
|
||||
},
|
||||
});
|
||||
console.log(` APBDes Item: ${item.kode} - ${item.uraian}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Realisasi Items...");
|
||||
for (const item of realisasiJson) {
|
||||
await prisma.realisasiItem.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
kode: item.kode,
|
||||
apbdesItemId: item.apbdesItemId,
|
||||
jumlah: item.jumlah,
|
||||
tanggal: new Date(item.tanggal),
|
||||
keterangan: item.keterangan,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
kode: item.kode,
|
||||
apbdesItemId: item.apbdesItemId,
|
||||
jumlah: item.jumlah,
|
||||
tanggal: new Date(item.tanggal),
|
||||
keterangan: item.keterangan,
|
||||
},
|
||||
});
|
||||
console.log(` Realisasi: ${item.kode} - Rp ${item.jumlah.toLocaleString("id-ID")}`);
|
||||
}
|
||||
|
||||
console.log("APBDes Item & Realisasi seed selesai");
|
||||
}
|
||||
27
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
27
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const demografiPekerjaan = loadJsonData("ekonomi/demografi-pekerjaan/demografi-pekerjaan.json");
|
||||
|
||||
export async function seedDemografiPekerjaan() {
|
||||
console.log("🔄 Seeding Demografi Pekerjaan...");
|
||||
for (const k of demografiPekerjaan) {
|
||||
await prisma.dataDemografiPekerjaan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
pekerjaan: k.pekerjaan,
|
||||
lakiLaki: k.lakiLaki,
|
||||
perempuan: k.perempuan,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
pekerjaan: k.pekerjaan,
|
||||
lakiLaki: k.lakiLaki,
|
||||
perempuan: k.perempuan,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Demografi Pekerjaan seeded successfully");
|
||||
}
|
||||
25
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
25
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const jumlahPendudukMiskin = loadJsonData("ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json");
|
||||
|
||||
export async function seedJumlahPendudukMiskin() {
|
||||
console.log("🔄 Seeding Jumlah Penduduk Miskin...");
|
||||
for (const k of jumlahPendudukMiskin) {
|
||||
await prisma.grafikJumlahPendudukMiskin.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
year: k.year,
|
||||
totalPoorPopulation: k.totalPoorPopulation,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
year: k.year,
|
||||
totalPoorPopulation: k.totalPoorPopulation,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jumlah Penduduk Miskin seeded successfully");
|
||||
}
|
||||
29
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
29
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const jumlahPengangguran = loadJsonData("ekonomi/jumlah-pengangguran/detail-data-pengangguran.json");
|
||||
|
||||
export async function seedJumlahPengangguran() {
|
||||
for (const d of jumlahPengangguran) {
|
||||
await prisma.detailDataPengangguran.upsert({
|
||||
where: {
|
||||
month_year: { month: d.month, year: d.year },
|
||||
},
|
||||
update: {
|
||||
totalUnemployment: d.totalUnemployment,
|
||||
educatedUnemployment: d.educatedUnemployment,
|
||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||
percentageChange: d.percentageChange,
|
||||
},
|
||||
create: {
|
||||
month: d.month,
|
||||
year: d.year,
|
||||
totalUnemployment: d.totalUnemployment,
|
||||
educatedUnemployment: d.educatedUnemployment,
|
||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||
percentageChange: d.percentageChange,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 detailDataPengangguran success ...");
|
||||
}
|
||||
37
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
37
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const lowonganKerjaLokal = loadJsonData("ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json");
|
||||
|
||||
export async function seedLowonganKerjaLokal() {
|
||||
console.log("🔄 Seeding Lowongan Kerja Lokal...");
|
||||
for (const k of lowonganKerjaLokal) {
|
||||
await prisma.lowonganPekerjaan.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
posisi: k.posisi,
|
||||
namaPerusahaan: k.namaPerusahaan,
|
||||
lokasi: k.lokasi,
|
||||
tipePekerjaan: k.tipePekerjaan,
|
||||
gaji: k.gaji,
|
||||
deskripsi: k.deskripsi,
|
||||
kualifikasi: k.kualifikasi,
|
||||
notelp: k.notelp,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
posisi: k.posisi,
|
||||
namaPerusahaan: k.namaPerusahaan,
|
||||
lokasi: k.lokasi,
|
||||
tipePekerjaan: k.tipePekerjaan,
|
||||
gaji: k.gaji,
|
||||
deskripsi: k.deskripsi,
|
||||
kualifikasi: k.kualifikasi,
|
||||
notelp: k.notelp,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Lowongan Kerja Lokal seeded successfully");
|
||||
}
|
||||
93
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
93
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kategoriProduk = loadJsonData("ekonomi/pasar-desa/kategori-produk.json");
|
||||
const pasarDesa = loadJsonData("ekonomi/pasar-desa/pasar-desa.json");
|
||||
const kategoriToPasar = loadJsonData("ekonomi/pasar-desa/kategori-to-pasar.json");
|
||||
|
||||
export async function seedPasarDesa() {
|
||||
console.log("🔄 Seeding Kategori Produk...");
|
||||
for (const k of kategoriProduk) {
|
||||
await prisma.kategoriProduk.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kategori Produk seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pasar Desa...");
|
||||
|
||||
for (const p of pasarDesa) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pasar desa "${p.nama}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.pasarDesa.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
nama: p.nama,
|
||||
deskripsi: p.deskripsi,
|
||||
harga: p.harga,
|
||||
rating: p.rating,
|
||||
alamatUsaha: p.alamatUsaha,
|
||||
kontak: p.kontak,
|
||||
imageId,
|
||||
kategoriProdukId: p.kategoriProdukId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
nama: p.nama,
|
||||
deskripsi: p.deskripsi,
|
||||
harga: p.harga,
|
||||
rating: p.rating,
|
||||
alamatUsaha: p.alamatUsaha,
|
||||
kontak: p.kontak,
|
||||
imageId,
|
||||
kategoriProdukId: p.kategoriProdukId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Pasar desa seeded: ${p.nama}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Pasar desa seed selesai");
|
||||
|
||||
console.log("🔄 Seeding Kategori To Pasar...");
|
||||
for (const p of kategoriToPasar) {
|
||||
await prisma.kategoriToPasar.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
kategoriId: p.kategoriId,
|
||||
pasarDesaId: p.pasarDesaId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
kategoriId: p.kategoriId,
|
||||
pasarDesaId: p.pasarDesaId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
83
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
83
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const apbdes = loadJsonData("ekonomi/pendapatan-asli-desa/apbDesa.json");
|
||||
const pendapatan = loadJsonData("ekonomi/pendapatan-asli-desa/pendapatanDesa.json");
|
||||
const belanja = loadJsonData("ekonomi/pendapatan-asli-desa/belanjaDesa.json");
|
||||
const pembiayaan = loadJsonData("ekonomi/pendapatan-asli-desa/pembiayaanDesa.json");
|
||||
|
||||
export async function seedPendapatanAsli() {
|
||||
console.log("🔄 Seeding Pendapatan Asli...");
|
||||
for (const d of apbdes) {
|
||||
await prisma.apbDesa.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
tahun: d.tahun,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
tahun: d.tahun,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pendapatan Asli seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pendapatan...");
|
||||
for (const d of pendapatan) {
|
||||
await prisma.pendapatan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pendapatan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Belanja...");
|
||||
for (const d of belanja) {
|
||||
await prisma.belanja.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Belanja seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Pembiayaan...");
|
||||
for (const d of pembiayaan) {
|
||||
await prisma.pembiayaan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
value: d.nilai
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pembiayaan seeded successfully");
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const grafikMenganggurBerdasarkanUsia = loadJsonData("ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json");
|
||||
const grafikMenganggurBerdasarkanPendidikan = loadJsonData("ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json");
|
||||
|
||||
export async function seedPendudukUsiaKerjaYangMenganggur() {
|
||||
for (const p of grafikMenganggurBerdasarkanUsia) {
|
||||
await prisma.grafikMenganggurBerdasarkanUsia.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
usia18_25: p.usia18_25,
|
||||
usia26_35: p.usia26_35,
|
||||
usia36_45: p.usia36_45,
|
||||
usia46_keatas: p.usia46_keatas,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
usia18_25: p.usia18_25,
|
||||
usia26_35: p.usia26_35,
|
||||
usia36_45: p.usia36_45,
|
||||
usia46_keatas: p.usia46_keatas,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
||||
for (const p of grafikMenganggurBerdasarkanPendidikan) {
|
||||
await prisma.grafikMenganggurBerdasarkanPendidikan.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
SD: p.SD,
|
||||
SMP: p.SMP,
|
||||
SMA: p.SMA,
|
||||
D3: p.D3,
|
||||
S1: p.S1,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
SD: p.SD,
|
||||
SMP: p.SMP,
|
||||
SMA: p.SMA,
|
||||
D3: p.D3,
|
||||
S1: p.S1,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
||||
}
|
||||
52
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
52
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const programKemiskinan = loadJsonData("ekonomi/program-kemiskinan/program-kemiskinan.json");
|
||||
const statistikKemiskinan = loadJsonData("ekonomi/program-kemiskinan/statistik-kemiskinan.json");
|
||||
|
||||
export async function seedProgramKemiskinan() {
|
||||
for (const s of statistikKemiskinan) {
|
||||
await prisma.statistikKemiskinan.upsert({
|
||||
where: { tahun: s.tahun }, // ✅ FIX
|
||||
update: {
|
||||
jumlah: s.jumlah,
|
||||
},
|
||||
create: {
|
||||
id: s.id, // id boleh tetap
|
||||
tahun: s.tahun,
|
||||
jumlah: s.jumlah,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("📊 Statistik Kemiskinan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Program Kemiskinan...");
|
||||
for (const k of programKemiskinan) {
|
||||
await prisma.programKemiskinan.upsert({
|
||||
where: { id: k.id },
|
||||
update: {
|
||||
nama: k.nama,
|
||||
deskripsi: k.deskripsi,
|
||||
icon: k.icon,
|
||||
statistik: {
|
||||
connect: {
|
||||
tahun: k.tahun, // 👈 BUKAN ID
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
deskripsi: k.deskripsi,
|
||||
icon: k.icon,
|
||||
statistik: {
|
||||
connect: {
|
||||
tahun: k.tahun,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Program Kemiskinan seeded successfully");
|
||||
}
|
||||
27
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
27
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const sektorUnggulanDesa = loadJsonData("ekonomi/sektor-unggulan/sektor-unggulan.json");
|
||||
|
||||
export async function seedSektorUnggulanDesa() {
|
||||
console.log("🔄 Seeding Sektor Unggulan Desa...");
|
||||
for (const k of sektorUnggulanDesa) {
|
||||
await prisma.sektorUnggulanDesa.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
name: k.name,
|
||||
description: k.description,
|
||||
value: k.value,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
description: k.description,
|
||||
value: k.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Sektor Unggulan Desa seeded successfully");
|
||||
}
|
||||
80
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
80
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
interface PosisiOrganisasi {
|
||||
id: string;
|
||||
nama: string;
|
||||
deskripsi: string;
|
||||
hierarki: number;
|
||||
parentId: string | null;
|
||||
}
|
||||
|
||||
interface PegawaiBumDes {
|
||||
id: string;
|
||||
namaLengkap: string;
|
||||
gelarAkademik: string;
|
||||
tanggalMasuk: string;
|
||||
email: string;
|
||||
telepon: string;
|
||||
alamat: string;
|
||||
posisiId: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const posisiOrganisasiBumDes = loadJsonData<PosisiOrganisasi[][]>("ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json");
|
||||
const pegawai = loadJsonData<PegawaiBumDes[]>("ekonomi/struktur-organisasi/pegawai-bumdes.json");
|
||||
|
||||
export async function seedStrukturBumdes() {
|
||||
const flattenedPosisi = posisiOrganisasiBumDes.flat();
|
||||
|
||||
// ✅ Urutkan berdasarkan hierarki
|
||||
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
|
||||
|
||||
for (const p of sortedPosisi) {
|
||||
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
||||
if (p.parentId) {
|
||||
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
|
||||
if (!parentExists) {
|
||||
console.warn(
|
||||
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
await prisma.posisiOrganisasiBumDes.upsert({
|
||||
where: { id: p.id },
|
||||
update: p,
|
||||
create: p,
|
||||
});
|
||||
}
|
||||
console.log("posisi organisasi berhasil");
|
||||
for (const p of pegawai) {
|
||||
await prisma.pegawaiBumDes.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
namaLengkap: p.namaLengkap,
|
||||
gelarAkademik: p.gelarAkademik,
|
||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||
email: p.email,
|
||||
telepon: p.telepon,
|
||||
alamat: p.alamat,
|
||||
posisiId: p.posisiId,
|
||||
isActive: p.isActive,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
namaLengkap: p.namaLengkap,
|
||||
gelarAkademik: p.gelarAkademik,
|
||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||
email: p.email,
|
||||
telepon: p.telepon,
|
||||
alamat: p.alamat,
|
||||
posisiId: p.posisiId,
|
||||
isActive: p.isActive,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("pegawai success ...");
|
||||
}
|
||||
33
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
33
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const ajukanIde = loadJsonData("inovasi/ajukan-ide/ajukan-ide.json");
|
||||
|
||||
export async function seedAjukan() {
|
||||
console.log("🔄 Seeding Ajukan Ide Inovatif...");
|
||||
for (const d of ajukanIde) {
|
||||
await prisma.ajukanIdeInovatif.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
namaIde: d.namaIde,
|
||||
deskripsi: d.deskripsi,
|
||||
masalah: d.masalah,
|
||||
benefit: d.benefit,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
namaIde: d.namaIde,
|
||||
deskripsi: d.deskripsi,
|
||||
masalah: d.masalah,
|
||||
benefit: d.benefit,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Ajukan Ide Inovatif seeded successfully");
|
||||
}
|
||||
44
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
44
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const desaDigital = loadJsonData("inovasi/desa-digital/desa-digital.json");
|
||||
|
||||
export async function seedDesaDigital() {
|
||||
console.log("🔄 Seeding Desa Digital...");
|
||||
for (const d of desaDigital) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (d.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: d.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for desa digital "${d.name}": ${d.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.desaDigital.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
deskripsi: d.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
deskripsi: d.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Desa Digital seeded successfully");
|
||||
}
|
||||
44
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
44
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const infoTeknologi = loadJsonData("inovasi/info-teknologi/info-teknologi.json");
|
||||
|
||||
export async function seedInfoTeknologi() {
|
||||
console.log("🔄 Seeding Info Teknologi...");
|
||||
for (const p of infoTeknologi) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for berita "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.infoTekno.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Info Teknologi seeded successfully");
|
||||
}
|
||||
68
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
68
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kolaborasiInovasi = loadJsonData("inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json");
|
||||
const mitraKolaborasi = loadJsonData("inovasi/kolaborasi-inovasi/mitra-kolaborasi.json");
|
||||
|
||||
export async function seedKolaborasiInovasi() {
|
||||
console.log("🔄 Seeding Kolaborasi Inovasi...");
|
||||
for (const p of kolaborasiInovasi) {
|
||||
await prisma.kolaborasiInovasi.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
tahun: p.tahun,
|
||||
slug: p.slug,
|
||||
deskripsi: p.deskripsi,
|
||||
kolaborator: p.kolaborator,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
tahun: p.tahun,
|
||||
slug: p.slug,
|
||||
deskripsi: p.deskripsi,
|
||||
kolaborator: p.kolaborator,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kolaborasi Inovasi seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Mitra Kolaborasi...");
|
||||
for (const p of mitraKolaborasi) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for mitra kolaborasi "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.mitraKolaborasi.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
imageId: imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
imageId: imageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Mitra Kolaborasi seeded successfully");
|
||||
}
|
||||
115
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
115
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const jenisLayanan = loadJsonData("inovasi/layanan-online-desa/jenis-layanan.json");
|
||||
const administrasiOnline = loadJsonData("inovasi/layanan-online-desa/administrasi-online.json");
|
||||
const jenisPengaduan = loadJsonData("inovasi/layanan-online-desa/jenis-pengaduan.json");
|
||||
const pengaduanMasyarakat = loadJsonData("inovasi/layanan-online-desa/pengaduan-masyarakat.json");
|
||||
|
||||
export async function seedLayananOnlineDesa() {
|
||||
console.log("🔄 Seeding Jenis Layanan...");
|
||||
for (const j of jenisLayanan) {
|
||||
await prisma.jenisLayanan.upsert({
|
||||
where: {
|
||||
id: j.id,
|
||||
},
|
||||
update: {
|
||||
nama: j.nama,
|
||||
deskripsi: j.deskripsi,
|
||||
},
|
||||
create: {
|
||||
id: j.id,
|
||||
nama: j.nama,
|
||||
deskripsi: j.deskripsi,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jenis Layanan seeded successfully");
|
||||
console.log("🔄 Seeding Administrasi Online...");
|
||||
for (const d of administrasiOnline) {
|
||||
await prisma.administrasiOnline.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
jenisLayananId: d.jenisLayananId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
alamat: d.alamat,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
jenisLayananId: d.jenisLayananId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Administrasi Online seeded successfully");
|
||||
console.log("🔄 Seeding Jenis Pengaduan Masyarakat...");
|
||||
for (const d of jenisPengaduan) {
|
||||
await prisma.jenisPengaduan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
nama: d.nama,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
nama: d.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Jenis Pengaduan Masyarakat seeded successfully");
|
||||
console.log("🔄 Seeding Pengaduan Masyarakat...");
|
||||
for (const d of pengaduanMasyarakat) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (d.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: d.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for pengaduan masyarakat "${d.name}": ${d.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.pengaduanMasyarakat.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
name: d.name,
|
||||
email: d.email,
|
||||
nik: d.nik,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
judulPengaduan: d.judulPengaduan,
|
||||
lokasiKejadian: d.lokasiKejadian,
|
||||
imageId: imageId,
|
||||
deskripsiPengaduan: d.deskripsiPengaduan,
|
||||
jenisPengaduanId: d.jenisPengaduanId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
email: d.email,
|
||||
nik: d.nik,
|
||||
nomorTelepon: d.nomorTelepon,
|
||||
judulPengaduan: d.judulPengaduan,
|
||||
lokasiKejadian: d.lokasiKejadian,
|
||||
imageId: imageId,
|
||||
deskripsiPengaduan: d.deskripsiPengaduan,
|
||||
jenisPengaduanId: d.jenisPengaduanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Pengaduan Masyarakat seeded successfully");
|
||||
}
|
||||
29
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
29
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const programKreatif = loadJsonData("inovasi/program-kreatif-desa/program-kreatif-desa.json");
|
||||
|
||||
export async function seedProgramKreatifDesa() {
|
||||
console.log("🔄 Seeding Program Kreatif...");
|
||||
for (const p of programKreatif) {
|
||||
await prisma.programKreatif.upsert({
|
||||
where: {
|
||||
id: p.id,
|
||||
},
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
slug: p.slug,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
icon: p.icon,
|
||||
slug: p.slug,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Program Kreatif seeded successfully");
|
||||
}
|
||||
46
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
46
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const keamananLingkunganJson = loadJsonData("keamanan/keamanan-lingkungan/keamanan-lingkungan.json");
|
||||
|
||||
export async function seedKeamananLingkungan() {
|
||||
console.log("🔄 Seeding Keamanan Lingkungan...");
|
||||
|
||||
for (const p of keamananLingkunganJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for keamanan lingkungan "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.keamananLingkungan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Keamanan lingkungan seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Keamanan lingkungan seed selesai");
|
||||
}
|
||||
89
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
89
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kontakDaruratKeamanan = loadJsonData("keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json");
|
||||
const kontakItem = loadJsonData("keamanan/kontak-darurat-keamanan/kontakItem.json");
|
||||
const kontakDaruratToItem = loadJsonData("keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json");
|
||||
|
||||
export async function seedKontakDaruratKeamanan() {
|
||||
console.log("🔄 Seeding Kontak Item...");
|
||||
for (const e of kontakItem) {
|
||||
await prisma.kontakItem.upsert({
|
||||
where: {
|
||||
id: e.id,
|
||||
},
|
||||
update: {
|
||||
nama: e.nama,
|
||||
icon: e.icon,
|
||||
nomorTelepon: e.nomorTelepon,
|
||||
},
|
||||
create: {
|
||||
id: e.id, // ✅ WAJIB
|
||||
nama: e.nama,
|
||||
icon: e.icon,
|
||||
nomorTelepon: e.nomorTelepon,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kontak Item seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Kontak Darurat Keamanan...");
|
||||
for (const d of kontakDaruratKeamanan) {
|
||||
await prisma.kontakDaruratKeamanan.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
nama: d.nama,
|
||||
icon: d.icon,
|
||||
kategoriId: d.kategoriId,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
nama: d.nama,
|
||||
icon: d.icon,
|
||||
kategoriId: d.kategoriId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Kontak Darurat Keamanan seeded successfully");
|
||||
|
||||
console.log("🔄 Seeding Kontak Darurat To Item...");
|
||||
for (const f of kontakDaruratToItem) {
|
||||
// ✅ Validasi foreign keys
|
||||
const kontakDaruratExists = await prisma.kontakDaruratKeamanan.findUnique({
|
||||
where: { id: f.kontakDaruratId },
|
||||
});
|
||||
|
||||
const kontakItemExists = await prisma.kontakItem.findUnique({
|
||||
where: { id: f.kontakItemId },
|
||||
});
|
||||
|
||||
if (!kontakDaruratExists) {
|
||||
console.warn(
|
||||
`⚠️ KontakDarurat ${f.kontakDaruratId} not found, skipping...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!kontakItemExists) {
|
||||
console.warn(`⚠️ KontakItem ${f.kontakItemId} not found, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.kontakDaruratToItem.upsert({
|
||||
where: { id: f.id },
|
||||
update: {
|
||||
kontakDaruratId: f.kontakDaruratId,
|
||||
kontakItemId: f.kontakItemId,
|
||||
},
|
||||
create: {
|
||||
id: f.id,
|
||||
kontakDaruratId: f.kontakDaruratId,
|
||||
kontakItemId: f.kontakItemId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("✅ Kontak Darurat To Item seeded successfully");
|
||||
}
|
||||
51
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
51
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const laporanPublik = loadJsonData("keamanan/laporan-publik/laporan-publik.json");
|
||||
const penangananLaporan = loadJsonData("keamanan/laporan-publik/penanganan-laporan.json");
|
||||
|
||||
export async function seedLaporanPublik() {
|
||||
console.log("🔄 Seeding Laporan Publik...");
|
||||
for (const l of laporanPublik) {
|
||||
await prisma.laporanPublik.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
judul: l.judul,
|
||||
lokasi: l.lokasi,
|
||||
tanggalWaktu: l.tanggalWaktu,
|
||||
kronologi: l.kronologi,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
judul: l.judul,
|
||||
lokasi: l.lokasi,
|
||||
tanggalWaktu: l.tanggalWaktu,
|
||||
kronologi: l.kronologi,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("laporan publik success ...");
|
||||
|
||||
console.log("🔄 Seeding Penanganan Laporan...");
|
||||
for (const l of penangananLaporan) {
|
||||
await prisma.penangananLaporanPublik.upsert({
|
||||
where: {
|
||||
id: l.id,
|
||||
},
|
||||
update: {
|
||||
deskripsi: l.deskripsi,
|
||||
laporanId: l.laporanId,
|
||||
},
|
||||
create: {
|
||||
id: l.id,
|
||||
deskripsi: l.deskripsi,
|
||||
laporanId: l.laporanId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("penanganan laporan success ...");
|
||||
}
|
||||
30
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
30
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const pencegahanKriminalitas = loadJsonData("keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json");
|
||||
|
||||
export async function seedPencegahanKriminalitas() {
|
||||
console.log("🔄 Seeding Pencegahan Kriminalitas...");
|
||||
for (const d of pencegahanKriminalitas) {
|
||||
await prisma.pencegahanKriminalitas.upsert({
|
||||
where: {
|
||||
id: d.id,
|
||||
},
|
||||
update: {
|
||||
judul: d.judul,
|
||||
deskripsi: d.deskripsi,
|
||||
deskripsiSingkat: d.deskripsiSingkat,
|
||||
linkVideo: d.linkVideo,
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
judul: d.judul,
|
||||
deskripsi: d.deskripsi,
|
||||
deskripsiSingkat: d.deskripsiSingkat,
|
||||
linkVideo: d.linkVideo,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("✅ Pencegahan Kriminalitas seeded successfully");
|
||||
}
|
||||
82
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
82
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const layananPolsek = loadJsonData("keamanan/polsek-terdekat/layanan-polsek.json");
|
||||
const polsekTerdekat = loadJsonData("keamanan/polsek-terdekat/polsek-terdekat.json");
|
||||
const layananToPolsek = loadJsonData("keamanan/polsek-terdekat/layanan-to-polsek.json");
|
||||
|
||||
export async function seedPolsekTerdekat() {
|
||||
console.log("🔄 Seeding Layanan Polsek...");
|
||||
for (const k of layananPolsek) {
|
||||
await prisma.layananPolsek.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("layanan polsek success ...");
|
||||
|
||||
console.log("🔄 Seeding Polsek Terdekat...");
|
||||
for (const k of polsekTerdekat) {
|
||||
await prisma.polsekTerdekat.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
nama: k.nama,
|
||||
jarakKeDesa: k.jarakKeDesa,
|
||||
alamat: k.alamat,
|
||||
nomorTelepon: k.nomorTelepon,
|
||||
jamOperasional: k.jamOperasional,
|
||||
embedMapUrl: k.embedMapUrl,
|
||||
namaTempatMaps: k.namaTempatMaps,
|
||||
alamatMaps: k.alamatMaps,
|
||||
linkPetunjukArah: k.linkPetunjukArah,
|
||||
layananPolsekId: k.layananPolsekId,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
nama: k.nama,
|
||||
jarakKeDesa: k.jarakKeDesa,
|
||||
alamat: k.alamat,
|
||||
nomorTelepon: k.nomorTelepon,
|
||||
jamOperasional: k.jamOperasional,
|
||||
embedMapUrl: k.embedMapUrl,
|
||||
namaTempatMaps: k.namaTempatMaps,
|
||||
alamatMaps: k.alamatMaps,
|
||||
linkPetunjukArah: k.linkPetunjukArah,
|
||||
layananPolsekId: k.layananPolsekId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("polsek terdekat success ...");
|
||||
|
||||
console.log("🔄 Seeding Layanan To Polsek...");
|
||||
for (const k of layananToPolsek) {
|
||||
await prisma.layananToPolsek.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
layananId: k.layananId,
|
||||
polsekTerdekatId: k.polsekTerdekatId,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
layananId: k.layananId,
|
||||
polsekTerdekatId: k.polsekTerdekatId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("layanan to polsek success ...");
|
||||
}
|
||||
46
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
46
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const tipsKeamananJson = loadJsonData("keamanan/tips-keamanan/tips-keamanan.json");
|
||||
|
||||
export async function seedTipsKeamanan() {
|
||||
console.log("🔄 Seeding Tips Keamanan...");
|
||||
|
||||
for (const p of tipsKeamananJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for tips keamanan "${p.judul}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.menuTipsKeamanan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
judul: p.judul,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Tips Keamanan seeded: ${p.judul}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Tips Keamanan seed selesai");
|
||||
}
|
||||
32
prisma/_seeder_list/kependudukan/seed_data_banjar.ts
Normal file
32
prisma/_seeder_list/kependudukan/seed_data_banjar.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const dataBanjarJson = loadJsonData("kependudukan/data-banjar/data-banjar.json");
|
||||
|
||||
export async function seedDataBanjar() {
|
||||
console.log("Seeding Data Banjar...");
|
||||
|
||||
for (const item of dataBanjarJson) {
|
||||
await prisma.dataBanjar.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
penduduk: item.penduduk,
|
||||
kk: item.kk,
|
||||
miskin: item.miskin,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
penduduk: item.penduduk,
|
||||
kk: item.kk,
|
||||
miskin: item.miskin,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
});
|
||||
console.log(` Banjar: ${item.nama} (${item.penduduk} penduduk)`);
|
||||
}
|
||||
|
||||
console.log("Data Banjar seed selesai");
|
||||
}
|
||||
32
prisma/_seeder_list/kependudukan/seed_dinamika_penduduk.ts
Normal file
32
prisma/_seeder_list/kependudukan/seed_dinamika_penduduk.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const dinamikaPendudukJson = loadJsonData("kependudukan/dinamika-penduduk/dinamika-penduduk.json");
|
||||
|
||||
export async function seedDinamikaPenduduk() {
|
||||
console.log("Seeding Dinamika Penduduk...");
|
||||
|
||||
for (const item of dinamikaPendudukJson) {
|
||||
await prisma.dinamikaPenduduk.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
tahun: item.tahun,
|
||||
kelahiran: item.kelahiran,
|
||||
kematian: item.kematian,
|
||||
masuk: item.masuk,
|
||||
keluar: item.keluar,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
tahun: item.tahun,
|
||||
kelahiran: item.kelahiran,
|
||||
kematian: item.kematian,
|
||||
masuk: item.masuk,
|
||||
keluar: item.keluar,
|
||||
},
|
||||
});
|
||||
console.log(` Tahun ${item.tahun}: ${item.kelahiran} kelahiran, ${item.kematian} kematian`);
|
||||
}
|
||||
|
||||
console.log("Dinamika Penduduk seed selesai");
|
||||
}
|
||||
28
prisma/_seeder_list/kependudukan/seed_distribusi_agama.ts
Normal file
28
prisma/_seeder_list/kependudukan/seed_distribusi_agama.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const distribusiAgamaJson = loadJsonData("kependudukan/distribusi-agama/distribusi-agama.json");
|
||||
|
||||
export async function seedDistribusiAgama() {
|
||||
console.log("Seeding Distribusi Agama...");
|
||||
|
||||
for (const item of distribusiAgamaJson) {
|
||||
await prisma.distribusiAgama.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
agama: item.agama,
|
||||
jumlah: item.jumlah,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
agama: item.agama,
|
||||
jumlah: item.jumlah,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
});
|
||||
console.log(` ${item.agama}: ${item.jumlah} penganut`);
|
||||
}
|
||||
|
||||
console.log("Distribusi Agama seed selesai");
|
||||
}
|
||||
28
prisma/_seeder_list/kependudukan/seed_distribusi_umur.ts
Normal file
28
prisma/_seeder_list/kependudukan/seed_distribusi_umur.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const distribusiUmurJson = loadJsonData("kependudukan/distribusi-umur/distribusi-umur.json");
|
||||
|
||||
export async function seedDistribusiUmur() {
|
||||
console.log("Seeding Distribusi Umur...");
|
||||
|
||||
for (const item of distribusiUmurJson) {
|
||||
await prisma.distribusiUmur.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
rentangUmur: item.rentangUmur,
|
||||
jumlah: item.jumlah,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
rentangUmur: item.rentangUmur,
|
||||
jumlah: item.jumlah,
|
||||
tahun: item.tahun,
|
||||
},
|
||||
});
|
||||
console.log(` Rentang ${item.rentangUmur}: ${item.jumlah} jiwa`);
|
||||
}
|
||||
|
||||
console.log("Distribusi Umur seed selesai");
|
||||
}
|
||||
34
prisma/_seeder_list/kependudukan/seed_migrasi_penduduk.ts
Normal file
34
prisma/_seeder_list/kependudukan/seed_migrasi_penduduk.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const migrasiPendudukJson = loadJsonData("kependudukan/migrasi-penduduk/migrasi-penduduk.json");
|
||||
|
||||
export async function seedMigrasiPenduduk() {
|
||||
console.log("Seeding MigrASI PENDUDUK...");
|
||||
|
||||
for (const item of migrasiPendudukJson) {
|
||||
await prisma.migrasiPenduduk.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
jenis: item.jenis,
|
||||
tanggal: new Date(item.tanggal),
|
||||
asal: item.asal,
|
||||
tujuan: item.tujuan,
|
||||
alasan: item.alasan,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
jenis: item.jenis,
|
||||
tanggal: new Date(item.tanggal),
|
||||
asal: item.asal,
|
||||
tujuan: item.tujuan,
|
||||
alasan: item.alasan,
|
||||
},
|
||||
});
|
||||
console.log(` ${item.nama}: ${item.jenis} (${item.alasan})`);
|
||||
}
|
||||
|
||||
console.log("Migrasi Penduduk seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const artikelJson = loadJsonData("kesehatan/artikel-kesehatan/artikel-kesehatan.json");
|
||||
const introJson = loadJsonData("kesehatan/artikel-kesehatan/introduction.json");
|
||||
const symptomJson = loadJsonData("kesehatan/artikel-kesehatan/symptom.json");
|
||||
const preventionJson = loadJsonData("kesehatan/artikel-kesehatan/prevention.json");
|
||||
const firstAidJson = loadJsonData("kesehatan/artikel-kesehatan/first-aid.json");
|
||||
const mythVsFactJson = loadJsonData("kesehatan/artikel-kesehatan/myth-vs-fact.json");
|
||||
const doctorSignJson = loadJsonData("kesehatan/artikel-kesehatan/doctor-sign.json");
|
||||
|
||||
export async function seedArtikelKesehatan() {
|
||||
console.log("Seeding Introduction...");
|
||||
for (const item of introJson) {
|
||||
await prisma.introduction.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Symptom...");
|
||||
for (const item of symptomJson) {
|
||||
await prisma.symptom.upsert({
|
||||
where: { id: item.id },
|
||||
update: { title: item.title, content: item.content },
|
||||
create: { id: item.id, title: item.title, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Prevention...");
|
||||
for (const item of preventionJson) {
|
||||
await prisma.prevention.upsert({
|
||||
where: { id: item.id },
|
||||
update: { title: item.title, content: item.content },
|
||||
create: { id: item.id, title: item.title, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding First Aid...");
|
||||
for (const item of firstAidJson) {
|
||||
await prisma.firstAid.upsert({
|
||||
where: { id: item.id },
|
||||
update: { title: item.title, content: item.content },
|
||||
create: { id: item.id, title: item.title, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Myth vs Fact...");
|
||||
for (const item of mythVsFactJson) {
|
||||
await prisma.mythVsFact.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
title: item.title,
|
||||
mitos: item.mitos,
|
||||
fakta: item.fakta,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
mitos: item.mitos,
|
||||
fakta: item.fakta,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Doctor Sign...");
|
||||
for (const item of doctorSignJson) {
|
||||
await prisma.doctorSign.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Artikel Kesehatan...");
|
||||
for (const item of artikelJson) {
|
||||
await prisma.artikelKesehatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
title: item.title,
|
||||
content: item.content,
|
||||
introductionId: item.introductionId,
|
||||
symptomId: item.symptomId,
|
||||
preventionId: item.preventionId,
|
||||
firstAidId: item.firstAidId,
|
||||
mythVsFactId: item.mythVsFactId,
|
||||
doctorSignId: item.doctorSignId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
content: item.content,
|
||||
introductionId: item.introductionId,
|
||||
symptomId: item.symptomId,
|
||||
preventionId: item.preventionId,
|
||||
firstAidId: item.firstAidId,
|
||||
mythVsFactId: item.mythVsFactId,
|
||||
doctorSignId: item.doctorSignId,
|
||||
},
|
||||
});
|
||||
console.log(` Artikel: ${item.title}`);
|
||||
}
|
||||
|
||||
console.log("Artikel Kesehatan seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const faskesJson = loadJsonData("kesehatan/fasilitas-kesehatan/fasilitas-kesehatan.json");
|
||||
const infoUmumJson = loadJsonData("kesehatan/fasilitas-kesehatan/informasi-umum.json");
|
||||
const layananUnggulanJson = loadJsonData("kesehatan/fasilitas-kesehatan/layanan-unggulan.json");
|
||||
const dokterJson = loadJsonData("kesehatan/fasilitas-kesehatan/dokter-tenaga-medis.json");
|
||||
const fasilitasPendukungJson = loadJsonData("kesehatan/fasilitas-kesehatan/fasilitas-pendukung.json");
|
||||
const prosedurJson = loadJsonData("kesehatan/fasilitas-kesehatan/prosedur-pendaftaran.json");
|
||||
const tarifJson = loadJsonData("kesehatan/fasilitas-kesehatan/tarif-layanan.json");
|
||||
|
||||
export async function seedFasilitasKesehatan() {
|
||||
console.log("Seeding Informasi Umum...");
|
||||
for (const item of infoUmumJson) {
|
||||
await prisma.informasiUmum.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
fasilitas: item.fasilitas,
|
||||
alamat: item.alamat,
|
||||
jamOperasional: item.jamOperasional,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
fasilitas: item.fasilitas,
|
||||
alamat: item.alamat,
|
||||
jamOperasional: item.jamOperasional,
|
||||
},
|
||||
});
|
||||
console.log(` Informasi Umum: ${item.fasilitas}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Layanan Unggulan...");
|
||||
for (const item of layananUnggulanJson) {
|
||||
await prisma.layananUnggulan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Fasilitas Pendukung...");
|
||||
for (const item of fasilitasPendukungJson) {
|
||||
await prisma.fasilitasPendukung.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Prosedur Pendaftaran...");
|
||||
for (const item of prosedurJson) {
|
||||
await prisma.prosedurPendaftaran.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Tarif dan Layanan...");
|
||||
for (const item of tarifJson) {
|
||||
await prisma.tarifDanLayanan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { layanan: item.layanan, tarif: item.tarif },
|
||||
create: { id: item.id, layanan: item.layanan, tarif: item.tarif },
|
||||
});
|
||||
console.log(` Tarif: ${item.layanan}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Dokter dan Tenaga Medis...");
|
||||
for (const item of dokterJson) {
|
||||
await prisma.dokterdanTenagaMedis.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
specialist: item.specialist,
|
||||
jadwal: item.jadwal,
|
||||
jadwalLibur: item.jadwalLibur,
|
||||
jamBukaOperasional: item.jamBukaOperasional,
|
||||
jamTutupOperasional: item.jamTutupOperasional,
|
||||
jamBukaLibur: item.jamBukaLibur,
|
||||
jamTutupLibur: item.jamTutupLibur,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
specialist: item.specialist,
|
||||
jadwal: item.jadwal,
|
||||
jadwalLibur: item.jadwalLibur,
|
||||
jamBukaOperasional: item.jamBukaOperasional,
|
||||
jamTutupOperasional: item.jamTutupOperasional,
|
||||
jamBukaLibur: item.jamBukaLibur,
|
||||
jamTutupLibur: item.jamTutupLibur,
|
||||
},
|
||||
});
|
||||
console.log(` Dokter: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Fasilitas Kesehatan...");
|
||||
for (const item of faskesJson) {
|
||||
await prisma.fasilitasKesehatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
informasiUmumId: item.informasiUmumId,
|
||||
layananUnggulanId: item.layananUnggulanId,
|
||||
fasilitasPendukungId: item.fasilitasPendukungId,
|
||||
prosedurPendaftaranId: item.prosedurPendaftaranId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
informasiUmumId: item.informasiUmumId,
|
||||
layananUnggulanId: item.layananUnggulanId,
|
||||
fasilitasPendukungId: item.fasilitasPendukungId,
|
||||
prosedurPendaftaranId: item.prosedurPendaftaranId,
|
||||
},
|
||||
});
|
||||
console.log(` Fasilitas Kesehatan: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("Fasilitas Kesehatan seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const infoWabahPenyakitJson = loadJsonData("kesehatan/infowabahpenyakit/infowabahpenyakit.json");
|
||||
|
||||
export async function seedInfoWabahPenyakit() {
|
||||
console.log("🔄 Seeding Info Wabah Penyakit...");
|
||||
|
||||
for (const p of infoWabahPenyakitJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for info wabah penyakit "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.infoWabahPenyakit.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsiLengkap: p.deskripsiLengkap,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsiLengkap: p.deskripsiLengkap,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Info wabah penyakit seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Info wabah penyakit seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const jadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/jadwal-kegiatan.json");
|
||||
const infoJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/informasi-jadwal.json");
|
||||
const deskJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/deskripsi-jadwal.json");
|
||||
const layananJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/layanan-jadwal.json");
|
||||
const syaratJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/syarat-ketentuan.json");
|
||||
const dokumenJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/dokumen-jadwal.json");
|
||||
const daftarJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/pendaftaran-jadwal.json");
|
||||
|
||||
export async function seedJadwalKegiatan() {
|
||||
console.log("Seeding Informasi Jadwal Kegiatan...");
|
||||
for (const item of infoJadwalJson) {
|
||||
await prisma.informasiJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
tanggal: item.tanggal,
|
||||
waktu: item.waktu,
|
||||
lokasi: item.lokasi,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
tanggal: item.tanggal,
|
||||
waktu: item.waktu,
|
||||
lokasi: item.lokasi,
|
||||
},
|
||||
});
|
||||
console.log(` Informasi: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Deskripsi Jadwal Kegiatan...");
|
||||
for (const item of deskJadwalJson) {
|
||||
await prisma.deskripsiJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { deskripsi: item.deskripsi },
|
||||
create: { id: item.id, deskripsi: item.deskripsi },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Layanan Jadwal Kegiatan...");
|
||||
for (const item of layananJadwalJson) {
|
||||
await prisma.layananJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Syarat & Ketentuan Jadwal...");
|
||||
for (const item of syaratJadwalJson) {
|
||||
await prisma.syaratKetentuanJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Dokumen Jadwal Kegiatan...");
|
||||
for (const item of dokumenJadwalJson) {
|
||||
await prisma.dokumenJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: { content: item.content },
|
||||
create: { id: item.id, content: item.content },
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding Pendaftaran Jadwal Kegiatan...");
|
||||
for (const item of daftarJadwalJson) {
|
||||
await prisma.pendaftaranJadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
name: item.name,
|
||||
tanggal: item.tanggal,
|
||||
namaOrangtua: item.namaOrtu,
|
||||
nomor: item.nomor,
|
||||
alamat: item.alamat,
|
||||
catatan: item.catatan,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
tanggal: item.tanggal,
|
||||
namaOrangtua: item.namaOrtu,
|
||||
nomor: item.nomor,
|
||||
alamat: item.alamat,
|
||||
catatan: item.catatan,
|
||||
},
|
||||
});
|
||||
console.log(` Pendaftaran: ${item.name}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Jadwal Kegiatan...");
|
||||
for (const item of jadwalJson) {
|
||||
await prisma.jadwalKegiatan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
content: item.content,
|
||||
informasiJadwalKegiatanId: item.informasiJadwalKegiatanId,
|
||||
deskripsiJadwalKegiatanId: item.deskripsiJadwalKegiatanId,
|
||||
layananJadwalKegiatanId: item.layananJadwalKegiatanId,
|
||||
syaratKetentuanJadwalKegiatanId: item.syaratKetentuanJadwalKegiatanId,
|
||||
dokumenJadwalKegiatanId: item.dokumenJadwalKegiatanId,
|
||||
pendaftaranJadwalKegiatanId: item.pendaftaranJadwalKegiatanId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
content: item.content,
|
||||
informasiJadwalKegiatanId: item.informasiJadwalKegiatanId,
|
||||
deskripsiJadwalKegiatanId: item.deskripsiJadwalKegiatanId,
|
||||
layananJadwalKegiatanId: item.layananJadwalKegiatanId,
|
||||
syaratKetentuanJadwalKegiatanId: item.syaratKetentuanJadwalKegiatanId,
|
||||
dokumenJadwalKegiatanId: item.dokumenJadwalKegiatanId,
|
||||
pendaftaranJadwalKegiatanId: item.pendaftaranJadwalKegiatanId,
|
||||
},
|
||||
});
|
||||
console.log(` Jadwal Kegiatan seeded`);
|
||||
}
|
||||
|
||||
console.log("Jadwal Kegiatan seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
import prisma from "@/lib/prisma";
|
||||
|
||||
const kontakDaruratJson = loadJsonData("kesehatan/kontak-darurat/kontak-darurat.json");
|
||||
|
||||
export async function seedKontakDarurat() {
|
||||
console.log("🔄 Seeding Kontak Darurat...");
|
||||
|
||||
for (const p of kontakDaruratJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for kontak darurat "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.kontakDarurat.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
whatsapp: p.whatsapp,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
whatsapp: p.whatsapp,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Kontak darurat seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Kontak darurat seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const penangananDaruratJson = loadJsonData("kesehatan/penanganan-darurat/penganan-darurat.json");
|
||||
|
||||
export async function seedPenangananDarurat() {
|
||||
console.log("🔄 Seeding Penanganan Darurat...");
|
||||
|
||||
for (const p of penangananDaruratJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for penanganan darurat "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.penangananDarurat.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Penanganan darurat seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Penanganan darurat seed selesai");
|
||||
}
|
||||
50
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
50
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const posyanduJson = loadJsonData("kesehatan/posyandu/posyandu.json");
|
||||
|
||||
export async function seedPosyandu() {
|
||||
console.log("🔄 Seeding Posyandu...");
|
||||
|
||||
for (const p of posyanduJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for posyandu "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.posyandu.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
nomor: p.nomor,
|
||||
deskripsi: p.deskripsi,
|
||||
jadwalPelayanan: p.jadwalPelayanan,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
nomor: p.nomor,
|
||||
deskripsi: p.deskripsi,
|
||||
jadwalPelayanan: p.jadwalPelayanan,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Posyandu seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Posyandu seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const programKesehatanJson = loadJsonData("kesehatan/program-kesehatan/program-kesehatan.json");
|
||||
|
||||
export async function seedProgramKesehatan() {
|
||||
for (const p of programKesehatanJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for program kesehatan "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.programKesehatan.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsiSingkat: p.deskripsiSingkat,
|
||||
deskripsi: p.deskripsi,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Program kesehatan seeded: ${p.name}`);
|
||||
}
|
||||
}
|
||||
97
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
97
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const puskesmasJson = loadJsonData("kesehatan/puskesmas/puskesmas.json");
|
||||
const kontakPuskesmasJson = loadJsonData("kesehatan/puskesmas/kontak-puskesmas/kontak.json");
|
||||
const jamPuskesmasJson = loadJsonData("kesehatan/puskesmas/jam-puskesmas/jam.json");
|
||||
|
||||
export async function seedPuskesmas() {
|
||||
console.log("🔄 Seeding Kontak Puskesmas...");
|
||||
for (const k of kontakPuskesmasJson) {
|
||||
await prisma.kontakPuskesmas.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
kontakPuskesmas: k.kontakPuskesmas,
|
||||
email: k.email,
|
||||
facebook: k.facebook,
|
||||
kontakUGD: k.kontakUGD,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
kontakPuskesmas: k.kontakPuskesmas,
|
||||
email: k.email,
|
||||
facebook: k.facebook,
|
||||
kontakUGD: k.kontakUGD,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("kontak puskesmas success ...");
|
||||
|
||||
console.log("🔄 Seeding Jam Puskesmas...");
|
||||
for (const k of jamPuskesmasJson) {
|
||||
await prisma.jamOperasional.upsert({
|
||||
where: {
|
||||
id: k.id,
|
||||
},
|
||||
update: {
|
||||
workDays: k.workDays,
|
||||
weekDays: k.weekDays,
|
||||
holiday: k.holiday,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
workDays: k.workDays,
|
||||
weekDays: k.weekDays,
|
||||
holiday: k.holiday,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("jam puskesmas success ...");
|
||||
|
||||
console.log("🔄 Seeding Puskesmas...");
|
||||
|
||||
for (const p of puskesmasJson) {
|
||||
let imageId: string | null = null;
|
||||
|
||||
if (p.imageName) {
|
||||
const image = await prisma.fileStorage.findUnique({
|
||||
where: { name: p.imageName },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!image) {
|
||||
console.warn(
|
||||
`⚠️ Image not found for puskesmas "${p.name}": ${p.imageName}`,
|
||||
);
|
||||
} else {
|
||||
imageId = image.id;
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.puskesmas.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
alamat: p.alamat,
|
||||
jamId: p.jamId,
|
||||
kontakId: p.kontakId,
|
||||
imageId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
alamat: p.alamat,
|
||||
jamId: p.jamId,
|
||||
kontakId: p.kontakId,
|
||||
imageId,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`✅ Puskesmas seeded: ${p.name}`);
|
||||
}
|
||||
|
||||
console.log("🎉 Puskesmas seed selesai");
|
||||
}
|
||||
|
||||
32
prisma/_seeder_list/kesehatan/seed_grafik_kepuasan.ts
Normal file
32
prisma/_seeder_list/kesehatan/seed_grafik_kepuasan.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const grafikKepuasanJson = loadJsonData("kesehatan/grafik-kepuasan/grafik-kepuasan.json");
|
||||
|
||||
export async function seedGrafikKepuasan() {
|
||||
console.log("Seeding Grafik Kepuasan...");
|
||||
|
||||
for (const item of grafikKepuasanJson) {
|
||||
await prisma.grafikKepuasan.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
penyakit: item.penyakit,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
penyakit: item.penyakit,
|
||||
},
|
||||
});
|
||||
console.log(` Grafik Kepuasan: ${item.nama}`);
|
||||
}
|
||||
|
||||
console.log("Grafik Kepuasan seed selesai");
|
||||
}
|
||||
70
prisma/_seeder_list/kesehatan/seed_kelahiran_kematian.ts
Normal file
70
prisma/_seeder_list/kesehatan/seed_kelahiran_kematian.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../load-json";
|
||||
|
||||
const kelahiranJson = loadJsonData("kesehatan/kelahiran/kelahiran.json");
|
||||
const kematianJson = loadJsonData("kesehatan/kematian/kematian.json");
|
||||
const dataKematianKelahiranJson = loadJsonData("kesehatan/kematian-kelahiran/data-kematian-kelahiran.json");
|
||||
|
||||
export async function seedKelahiranKematian() {
|
||||
console.log("Seeding Kelahiran...");
|
||||
for (const item of kelahiranJson) {
|
||||
await prisma.kelahiran.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
},
|
||||
});
|
||||
console.log(` Kelahiran: ${item.nama}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Kematian...");
|
||||
for (const item of kematianJson) {
|
||||
await prisma.kematian.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
penyebab: item.penyebab,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
tanggal: new Date(item.tanggal),
|
||||
jenisKelamin: item.jenisKelamin,
|
||||
alamat: item.alamat,
|
||||
penyebab: item.penyebab,
|
||||
},
|
||||
});
|
||||
console.log(` Kematian: ${item.nama}`);
|
||||
}
|
||||
|
||||
console.log("Seeding Data Kematian-Kelahiran...");
|
||||
for (const item of dataKematianKelahiranJson) {
|
||||
await prisma.dataKematian_Kelahiran.upsert({
|
||||
where: { id: item.id },
|
||||
update: {
|
||||
kematianId: item.kematianId,
|
||||
kelahiranId: item.kelahiranId,
|
||||
},
|
||||
create: {
|
||||
id: item.id,
|
||||
kematianId: item.kematianId,
|
||||
kelahiranId: item.kelahiranId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Kelahiran & Kematian seed selesai");
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const kategoriDesaAntiKorupsi = loadJsonData("landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json");
|
||||
const desaAntiKorupsi = loadJsonData("landing-page/desa-anti-korupsi/desaantiKorpusi.json");
|
||||
|
||||
export async function seedDesaAntiKorupsi() {
|
||||
for (const k of kategoriDesaAntiKorupsi) {
|
||||
await prisma.kategoriDesaAntiKorupsi.upsert({
|
||||
where: { id: k.id },
|
||||
update: {
|
||||
name: k.name,
|
||||
},
|
||||
create: {
|
||||
id: k.id,
|
||||
name: k.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("kategori desa anti korupsi success ...");
|
||||
|
||||
// =========== DESA ANTI KORUPSI ===========
|
||||
for (const p of desaAntiKorupsi) {
|
||||
await prisma.desaAntiKorupsi.upsert({
|
||||
where: { id: p.id },
|
||||
update: {
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
kategoriId: p.kategoriId,
|
||||
},
|
||||
create: {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
deskripsi: p.deskripsi,
|
||||
kategoriId: p.kategoriId,
|
||||
},
|
||||
});
|
||||
}
|
||||
console.log("desa anti korupsi success ...");
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const prestasiDesa = loadJsonData("landing-page/prestasi-desa/prestasi-desa.json");
|
||||
const kategoriPrestasiDesa = loadJsonData("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,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const mediaSosial = loadJsonData("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,42 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const profilePejabatDesa = loadJsonData("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,46 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const programInovasi = loadJsonData("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}`);
|
||||
}
|
||||
}
|
||||
43
prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts
Normal file
43
prisma/_seeder_list/landing-page/sdgs/seed_sdgs.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { loadJsonData } from "../../../load-json";
|
||||
|
||||
const sdgsDesa = loadJsonData("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 ...");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user