Compare commits
192 Commits
nico/30-ja
...
tasks/add-
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 273dfdfd09 | |||
| 1d1d8e50dc | |||
| 092afe67d2 | |||
| 2d9170705d | |||
| fdf9a951a4 | |||
| ca74029688 | |||
| 1a8fc1a670 | |||
| 19235f0791 | |||
| 61de7d8d33 | |||
| 8fb85ce56c | |||
| 1f98b6993d | |||
| f3a10d63d1 | |||
| 7a42bec63b | |||
| 44c421129e | |||
| ddff427926 | |||
| 00c8caade4 | |||
| 0209f49449 | |||
| 344c6ada6d | |||
| 11acd04419 | |||
| 8d49213b68 | |||
| 96911e3cf1 | |||
| 9950c28b9b | |||
| fa0f3538d1 | |||
| 2778f53aff | |||
| 37ac91d4f4 | |||
| 217f4a9a3b | |||
| 5d6a7437ed | |||
| 752a6cabee | |||
| 134ddc6154 | |||
| 28979c6b49 | |||
| b2066caa13 | |||
| 023c77d636 | |||
| 9bf3ec72cf | |||
| f359f5b1ce | |||
| 1c1e8fb190 | |||
| 54f83da3b8 | |||
| f8985c550f | |||
| e3d909e760 | |||
| 16a8df50c1 | |||
| d66a952d4c |
47
.dockerignore
Normal file
47
.dockerignore
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
bun-debug.log*
|
||||||
|
|
||||||
|
# Docker files
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Markdown/Documentation
|
||||||
|
README.md
|
||||||
|
GEMINI.md
|
||||||
|
AGENTS.md
|
||||||
|
AUDIT_REPORT.md
|
||||||
|
QWEN.md
|
||||||
|
NOTE.md
|
||||||
|
task-project-apbdes.md
|
||||||
|
MUSIK_CREATE_ANALYSIS.md
|
||||||
|
darkMode.md
|
||||||
|
/test-results
|
||||||
|
/playwright-report
|
||||||
|
/tmp_assets
|
||||||
|
/foldergambar
|
||||||
|
/googleapi
|
||||||
|
/xx
|
||||||
|
/xx.ts
|
||||||
|
/xx.txt
|
||||||
|
/test.txt
|
||||||
|
/x.json
|
||||||
|
/x.sh
|
||||||
|
/xcoba.ts
|
||||||
|
/xcoba2.ts
|
||||||
|
/gambar.ttx
|
||||||
|
/test-berita-state.ts
|
||||||
44
.env.example
Normal file
44
.env.example
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Database Configuration
|
||||||
|
DATABASE_URL="postgresql://username:password@localhost:5432/desa-darmasaba?schema=public"
|
||||||
|
|
||||||
|
# Seafile Configuration (File Storage)
|
||||||
|
SEAFILE_TOKEN=your_seafile_token
|
||||||
|
SEAFILE_REPO_ID=your_seafile_repo_id
|
||||||
|
SEAFILE_URL=https://your-seafile-instance.com
|
||||||
|
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
||||||
|
|
||||||
|
# Upload Configuration
|
||||||
|
WIBU_UPLOAD_DIR=uploads
|
||||||
|
WIBU_DOWNLOAD_DIR=./download
|
||||||
|
|
||||||
|
# WhatsApp Server Configuration
|
||||||
|
WA_SERVER_TOKEN=your_whatsapp_server_token
|
||||||
|
|
||||||
|
# Application Configuration
|
||||||
|
# IMPORTANT: For staging/production, set this to your actual domain
|
||||||
|
# Local development: NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||||
|
# Staging: NEXT_PUBLIC_BASE_URL=https://desa-darmasaba-stg.wibudev.com
|
||||||
|
# Production: NEXT_PUBLIC_BASE_URL=https://your-production-domain.com
|
||||||
|
# Or use relative URL '/' for automatic protocol/domain detection (recommended)
|
||||||
|
NEXT_PUBLIC_BASE_URL=/
|
||||||
|
|
||||||
|
# Email Configuration (for notifications/subscriptions)
|
||||||
|
EMAIL_USER=your_email@gmail.com
|
||||||
|
EMAIL_PASS=your_email_app_password
|
||||||
|
|
||||||
|
# Session Configuration
|
||||||
|
BASE_SESSION_KEY=your_session_key_generate_secure_random_string
|
||||||
|
BASE_TOKEN_KEY=your_jwt_secret_key_generate_secure_random_string
|
||||||
|
|
||||||
|
# Telegram Bot Configuration (for notifications)
|
||||||
|
BOT_TOKEN=your_telegram_bot_token
|
||||||
|
CHAT_ID=your_telegram_chat_id
|
||||||
|
|
||||||
|
# Session Password (for iron-session)
|
||||||
|
SESSION_PASSWORD="your_session_password_min_32_characters_long_secure"
|
||||||
|
|
||||||
|
# ElevenLabs API Key (for TTS features - optional)
|
||||||
|
ELEVENLABS_API_KEY=your_elevenlabs_api_key
|
||||||
|
|
||||||
|
# Environment (optional, defaults to development)
|
||||||
|
# NODE_ENV=development
|
||||||
81
.gemini/hooks/telegram-notify.ts
Executable file
81
.gemini/hooks/telegram-notify.ts
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import { readFileSync, existsSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
// Function to manually load .env from project root if process.env is missing keys
|
||||||
|
function loadEnv() {
|
||||||
|
const envPath = join(process.cwd(), ".env");
|
||||||
|
if (existsSync(envPath)) {
|
||||||
|
const envContent = readFileSync(envPath, "utf-8");
|
||||||
|
const lines = envContent.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line && !line.startsWith("#")) {
|
||||||
|
const [key, ...valueParts] = line.split("=");
|
||||||
|
if (key && valueParts.length > 0) {
|
||||||
|
const value = valueParts.join("=").trim().replace(/^["']|["']$/g, "");
|
||||||
|
process.env[key.trim()] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
// Ensure environment variables are loaded
|
||||||
|
loadEnv();
|
||||||
|
|
||||||
|
const inputRaw = readFileSync(0, "utf-8");
|
||||||
|
if (!inputRaw) return;
|
||||||
|
|
||||||
|
let finalText = "";
|
||||||
|
let sessionId = "web-desa-darmasaba";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try parsing as JSON first
|
||||||
|
const input = JSON.parse(inputRaw);
|
||||||
|
sessionId = input.session_id || "web-desa-darmasaba";
|
||||||
|
finalText = typeof input === "string" ? input : (input.response || input.text || JSON.stringify(input));
|
||||||
|
} catch {
|
||||||
|
// If not JSON, use raw text
|
||||||
|
finalText = inputRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOT_TOKEN = process.env.BOT_TOKEN;
|
||||||
|
const CHAT_ID = process.env.CHAT_ID;
|
||||||
|
|
||||||
|
if (!BOT_TOKEN || !CHAT_ID) {
|
||||||
|
console.error("Missing BOT_TOKEN or CHAT_ID in environment variables");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message =
|
||||||
|
`✅ *Gemini Task Selesai*\n\n` +
|
||||||
|
`🆔 Session: \`${sessionId}\` \n\n` +
|
||||||
|
`🧠 Output:\n${finalText.substring(0, 3500)}`;
|
||||||
|
|
||||||
|
const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
chat_id: CHAT_ID,
|
||||||
|
text: message,
|
||||||
|
parse_mode: "Markdown",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorData = await res.json();
|
||||||
|
console.error("Telegram API Error:", errorData);
|
||||||
|
} else {
|
||||||
|
console.log("Notification sent successfully!");
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(JSON.stringify({ status: "continue" }));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Hook Error:", err);
|
||||||
|
process.stdout.write(JSON.stringify({ status: "continue" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
17
.gemini/settings.json
Normal file
17
.gemini/settings.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"AfterAgent": [
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"name": "telegram-notify",
|
||||||
|
"type": "command",
|
||||||
|
"command": "bun $GEMINI_PROJECT_DIR/.gemini/hooks/telegram-notify.ts",
|
||||||
|
"timeout": 10000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
219
.github/workflows/build.yml
vendored
219
.github/workflows/build.yml
vendored
@@ -1,219 +0,0 @@
|
|||||||
name: Build And Save Log
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
environment:
|
|
||||||
description: "Target environment (e.g., staging, production)"
|
|
||||||
required: true
|
|
||||||
default: "staging"
|
|
||||||
version:
|
|
||||||
description: "Version to deploy"
|
|
||||||
required: false
|
|
||||||
default: "latest"
|
|
||||||
|
|
||||||
env:
|
|
||||||
APP_NAME: desa-darmasaba-action
|
|
||||||
WA_PHONE: "6289697338821,6289697338822"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:14
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
|
||||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
||||||
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
options: >-
|
|
||||||
--health-cmd="pg_isready"
|
|
||||||
--health-interval=10s
|
|
||||||
--health-timeout=5s
|
|
||||||
--health-retries=5
|
|
||||||
|
|
||||||
steps:
|
|
||||||
# Checkout kode sumber
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Setup Bun
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
# Cache dependencies
|
|
||||||
- name: Cache dependencies
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: .bun
|
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lockb') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
# Step 1: Set BRANCH_NAME based on event type
|
|
||||||
- name: Set BRANCH_NAME
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
||||||
echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 2: Generate APP_VERSION dynamically
|
|
||||||
- name: Set APP_VERSION
|
|
||||||
run: echo "APP_VERSION=${{ github.sha }}---$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Step 3: Kirim notifikasi ke API build Start
|
|
||||||
- name: Notify start build
|
|
||||||
run: |
|
|
||||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
|
||||||
for PHONE in "${PHONES[@]}"; do
|
|
||||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:start\nApp:${{ env.APP_NAME }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}'))")
|
|
||||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install
|
|
||||||
|
|
||||||
# Konfigurasi environment variable untuk PostgreSQL dan variabel tambahan
|
|
||||||
- name: Set up environment variables
|
|
||||||
run: |
|
|
||||||
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
|
|
||||||
echo "PORT=3000" >> .env
|
|
||||||
echo "NEXT_PUBLIC_WIBU_URL=localhost:3000" >> .env
|
|
||||||
echo "WIBU_UPLOAD_DIR=/uploads" >> .env
|
|
||||||
|
|
||||||
# Create log file
|
|
||||||
- name: Create log file
|
|
||||||
run: touch build.txt
|
|
||||||
|
|
||||||
# Migrasi database menggunakan Prisma
|
|
||||||
- name: Apply Prisma schema to database
|
|
||||||
run: bun prisma db push >> build.txt 2>&1
|
|
||||||
|
|
||||||
# Seed database (opsional)
|
|
||||||
- name: Seed database
|
|
||||||
run: |
|
|
||||||
bun prisma db seed >> build.txt 2>&1 || echo "Seed failed or no seed data found. Continuing without seed." >> build.txt
|
|
||||||
|
|
||||||
# Build project
|
|
||||||
- name: Build project
|
|
||||||
run: bun run build >> build.txt 2>&1
|
|
||||||
|
|
||||||
# Ensure project directory exists
|
|
||||||
- name: Ensure /var/www/projects/${{ env.APP_NAME }} exists
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_HOST }}
|
|
||||||
username: ${{ secrets.VPS_USERNAME }}
|
|
||||||
key: ${{ secrets.VPS_SSH_KEY }}
|
|
||||||
script: |
|
|
||||||
mkdir -p /var/www/projects/${{ env.APP_NAME }}
|
|
||||||
|
|
||||||
# Deploy to a new version directory
|
|
||||||
- name: Deploy to VPS (New Version)
|
|
||||||
uses: appleboy/scp-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_HOST }}
|
|
||||||
username: ${{ secrets.VPS_USERNAME }}
|
|
||||||
key: ${{ secrets.VPS_SSH_KEY }}
|
|
||||||
source: "."
|
|
||||||
target: "/var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}"
|
|
||||||
|
|
||||||
# Set up environment variables
|
|
||||||
- name: Set up environment variables
|
|
||||||
run: |
|
|
||||||
rm -r .env
|
|
||||||
echo "DATABASE_URL=postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5433/${{ secrets.POSTGRES_DB }}?schema=public" >> .env
|
|
||||||
echo "NEXT_PUBLIC_WIBU_URL=${{ env.APP_NAME }}" >> .env
|
|
||||||
echo "WIBU_UPLOAD_DIR=/var/www/projects/${{ env.APP_NAME }}/uploads" >> .env
|
|
||||||
|
|
||||||
# Kirim file .env ke server
|
|
||||||
- name: Upload .env to server
|
|
||||||
uses: appleboy/scp-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_HOST }}
|
|
||||||
username: ${{ secrets.VPS_USERNAME }}
|
|
||||||
key: ${{ secrets.VPS_SSH_KEY }}
|
|
||||||
source: ".env"
|
|
||||||
target: "/var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}/"
|
|
||||||
|
|
||||||
# manage deployment
|
|
||||||
- name: manage deployment
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.VPS_HOST }}
|
|
||||||
username: ${{ secrets.VPS_USERNAME }}
|
|
||||||
key: ${{ secrets.VPS_SSH_KEY }}
|
|
||||||
script: |
|
|
||||||
|
|
||||||
# Source ~/.bashrc
|
|
||||||
source ~/.bashrc
|
|
||||||
|
|
||||||
# Find an available port
|
|
||||||
PORT=$(curl -s -X GET https://wibu-bot.wibudev.com/api/find-port | jq -r '.[0]')
|
|
||||||
if [ -z "$PORT" ] || ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
|
|
||||||
echo "Invalid or missing port from API."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# manage deployment
|
|
||||||
cd /var/www/projects/${{ env.APP_NAME }}/releases/${{ env.APP_VERSION }}
|
|
||||||
|
|
||||||
# Create uploads directory
|
|
||||||
mkdir -p /var/www/projects/${{ env.APP_NAME }}/uploads
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
bun install --production
|
|
||||||
|
|
||||||
# Apply database schema
|
|
||||||
if ! bun prisma db push; then
|
|
||||||
echo "Database migration failed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Seed database (optional)
|
|
||||||
bun prisma db seed || echo "tidak membutuhkan seed"
|
|
||||||
|
|
||||||
# Restart the application
|
|
||||||
pm2 reload ${{ env.APP_NAME }} || pm2 start "bun run start --port $PORT" --name "${{ env.APP_NAME }}-$PORT" --namespace "${{ env.APP_NAME }}"
|
|
||||||
|
|
||||||
# Step 4: Set BUILD_STATUS based on success or failure
|
|
||||||
- name: Set BUILD_STATUS
|
|
||||||
if: success()
|
|
||||||
run: echo "BUILD_STATUS=success" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set BUILD_STATUS on failure
|
|
||||||
if: failure()
|
|
||||||
run: echo "BUILD_STATUS=failed" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Update status log
|
|
||||||
- name: Update status log
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
echo "=====================" >> build.txt
|
|
||||||
echo "BUILD_STATUS=${{ env.BUILD_STATUS }}" >> build.txt
|
|
||||||
echo "APP_NAME=${{ env.APP_NAME }}" >> build.txt
|
|
||||||
echo "APP_VERSION=${{ env.APP_VERSION }}" >> build.txt
|
|
||||||
echo "=====================" >> build.txt
|
|
||||||
|
|
||||||
# Upload log to 0x0.st
|
|
||||||
- name: Upload log to 0x0.st
|
|
||||||
id: upload_log
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
LOG_URL=$(curl -F "file=@build.txt" https://wibu-bot.wibudev.com/api/file )
|
|
||||||
echo "LOG_URL=$LOG_URL" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Kirim notifikasi ke API
|
|
||||||
- name: Notify build success via API
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
|
||||||
for PHONE in "${PHONES[@]}"; do
|
|
||||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:${{ env.BUILD_STATUS }}\nApp:${{ env.APP_NAME }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}\nLog:${{ env.LOG_URL }}'))")
|
|
||||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
|
||||||
done
|
|
||||||
56
.github/workflows/docker-publish.yml
vendored
Normal file
56
.github/workflows/docker-publish.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Publish Docker to GHCR
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Build & Push to GHCR
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Free disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf /usr/local/lib/android
|
||||||
|
sudo rm -rf /opt/ghc
|
||||||
|
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||||
|
sudo docker image prune --all --force
|
||||||
|
df -h
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Extract tag name
|
||||||
|
id: meta
|
||||||
|
run: echo "tag=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
106
.github/workflows/publish.yml
vendored
Normal file
106
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
name: Publish Docker to GHCR
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stack_env:
|
||||||
|
description: "stack env"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: "dev"
|
||||||
|
options:
|
||||||
|
- dev
|
||||||
|
- prod
|
||||||
|
- stg
|
||||||
|
tag:
|
||||||
|
description: "Image tag (e.g. 1.0.0)"
|
||||||
|
required: true
|
||||||
|
default: "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Free disk space
|
||||||
|
run: |
|
||||||
|
sudo rm -rf /usr/share/dotnet
|
||||||
|
sudo rm -rf /usr/local/lib/android
|
||||||
|
sudo rm -rf /opt/ghc
|
||||||
|
sudo rm -rf /opt/hostedtoolcache/CodeQL
|
||||||
|
sudo docker image prune --all --force
|
||||||
|
df -h
|
||||||
|
|
||||||
|
- name: Checkout branch ${{ github.event.inputs.stack_env }}
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.inputs.stack_env }}
|
||||||
|
|
||||||
|
- name: Checkout scripts from main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
path: .ci
|
||||||
|
sparse-checkout: .github/workflows/script
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Generate image metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
|
||||||
|
type=raw,value=${{ github.event.inputs.stack_env }}-latest
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
no-cache: true
|
||||||
|
|
||||||
|
- name: Notify success
|
||||||
|
if: success()
|
||||||
|
run: bash ./.ci/.github/workflows/script/notify.sh
|
||||||
|
env:
|
||||||
|
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
NOTIFY_STATUS: success
|
||||||
|
NOTIFY_WORKFLOW: "Publish Docker"
|
||||||
|
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
|
||||||
|
|
||||||
|
- name: Notify failure
|
||||||
|
if: failure()
|
||||||
|
run: bash ./.ci/.github/workflows/script/notify.sh
|
||||||
|
env:
|
||||||
|
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
NOTIFY_STATUS: failure
|
||||||
|
NOTIFY_WORKFLOW: "Publish Docker"
|
||||||
|
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
|
||||||
60
.github/workflows/re-pull.yml
vendored
Normal file
60
.github/workflows/re-pull.yml
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
name: Re-Pull Docker
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stack_name:
|
||||||
|
description: "stack name"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
stack_env:
|
||||||
|
description: "stack env"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: "dev"
|
||||||
|
options:
|
||||||
|
- dev
|
||||||
|
- stg
|
||||||
|
- prod
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Re-Pull Docker ${{ github.event.inputs.stack_name }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout scripts from main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
sparse-checkout: .github/workflows/script
|
||||||
|
|
||||||
|
- name: Deploy ke Portainer
|
||||||
|
run: bash ./.github/workflows/script/re-pull.sh
|
||||||
|
env:
|
||||||
|
PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }}
|
||||||
|
PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }}
|
||||||
|
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
|
||||||
|
STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}
|
||||||
|
|
||||||
|
- name: Notify success
|
||||||
|
if: success()
|
||||||
|
run: bash ./.github/workflows/script/notify.sh
|
||||||
|
env:
|
||||||
|
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
NOTIFY_STATUS: success
|
||||||
|
NOTIFY_WORKFLOW: "Re-Pull Docker"
|
||||||
|
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
|
||||||
|
|
||||||
|
- name: Notify failure
|
||||||
|
if: failure()
|
||||||
|
run: bash ./.github/workflows/script/notify.sh
|
||||||
|
env:
|
||||||
|
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
|
||||||
|
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
NOTIFY_STATUS: failure
|
||||||
|
NOTIFY_WORKFLOW: "Re-Pull Docker"
|
||||||
|
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
|
||||||
26
.github/workflows/script/notify.sh
vendored
Normal file
26
.github/workflows/script/notify.sh
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}"
|
||||||
|
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}"
|
||||||
|
: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}"
|
||||||
|
: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}"
|
||||||
|
|
||||||
|
if [ "$NOTIFY_STATUS" = "success" ]; then
|
||||||
|
ICON="✅"
|
||||||
|
TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!"
|
||||||
|
else
|
||||||
|
ICON="❌"
|
||||||
|
TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$NOTIFY_DETAIL" ]; then
|
||||||
|
TEXT="${TEXT}
|
||||||
|
${NOTIFY_DETAIL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg chat_id "$TELEGRAM_CHAT_ID" \
|
||||||
|
--arg text "$TEXT" \
|
||||||
|
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')"
|
||||||
120
.github/workflows/script/re-pull.sh
vendored
Normal file
120
.github/workflows/script/re-pull.sh
vendored
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}"
|
||||||
|
: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}"
|
||||||
|
: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}"
|
||||||
|
: "${STACK_NAME:?STACK_NAME tidak di-set}"
|
||||||
|
|
||||||
|
# Timeout total: MAX_RETRY * SLEEP_INTERVAL detik
|
||||||
|
MAX_RETRY=60 # 60 × 10s = 10 menit
|
||||||
|
SLEEP_INTERVAL=10
|
||||||
|
|
||||||
|
echo "🔐 Autentikasi ke Portainer..."
|
||||||
|
TOKEN=$(curl -s -X POST "https://${PORTAINER_URL}/api/auth" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \
|
||||||
|
| jq -r .jwt)
|
||||||
|
|
||||||
|
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
||||||
|
echo "❌ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔍 Mencari stack: $STACK_NAME..."
|
||||||
|
STACK=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}" \
|
||||||
|
| jq ".[] | select(.Name == \"$STACK_NAME\")")
|
||||||
|
|
||||||
|
if [ -z "$STACK" ]; then
|
||||||
|
echo "❌ Stack '$STACK_NAME' tidak ditemukan di Portainer!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
STACK_ID=$(echo "$STACK" | jq -r .Id)
|
||||||
|
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
|
||||||
|
ENV=$(echo "$STACK" | jq '.Env // []')
|
||||||
|
|
||||||
|
# ── Catat container ID lama sebelum redeploy ──────────────────────────────────
|
||||||
|
echo "📸 Mencatat container aktif sebelum redeploy..."
|
||||||
|
CONTAINERS_BEFORE=$(curl -s -X GET \
|
||||||
|
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}")
|
||||||
|
|
||||||
|
OLD_IDS=$(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Id] | join(",")')
|
||||||
|
echo " Container lama: $(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Names[0]] | join(", ")')"
|
||||||
|
|
||||||
|
# ── Ambil compose file lalu trigger redeploy ─────────────────────────────────
|
||||||
|
echo "📄 Mengambil compose file..."
|
||||||
|
STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}" \
|
||||||
|
| jq -r .StackFileContent)
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg content "$STACK_FILE" \
|
||||||
|
--argjson env "$ENV" \
|
||||||
|
'{stackFileContent: $content, env: $env, pullImage: true}')
|
||||||
|
|
||||||
|
echo "🚀 Triggering redeploy $STACK_NAME (pull latest image)..."
|
||||||
|
HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \
|
||||||
|
-X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD")
|
||||||
|
|
||||||
|
if [ "$HTTP_STATUS" != "200" ]; then
|
||||||
|
echo "❌ Redeploy gagal! HTTP Status: $HTTP_STATUS"
|
||||||
|
cat /tmp/portainer_response.json | jq .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "⏳ Menunggu image selesai di-pull dan container baru running..."
|
||||||
|
echo " (Timeout: $((MAX_RETRY * SLEEP_INTERVAL)) detik)"
|
||||||
|
|
||||||
|
COUNT=0
|
||||||
|
while [ $COUNT -lt $MAX_RETRY ]; do
|
||||||
|
sleep $SLEEP_INTERVAL
|
||||||
|
COUNT=$((COUNT + 1))
|
||||||
|
|
||||||
|
CONTAINERS=$(curl -s -X GET \
|
||||||
|
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}")
|
||||||
|
|
||||||
|
# Container baru = ID tidak ada di daftar container lama
|
||||||
|
NEW_RUNNING=$(echo "$CONTAINERS" | jq \
|
||||||
|
--arg old "$OLD_IDS" \
|
||||||
|
'[.[] | select(.State == "running" and ((.Id) as $id | ($old | split(",") | index($id)) == null))] | length')
|
||||||
|
|
||||||
|
FAILED=$(echo "$CONTAINERS" | jq \
|
||||||
|
'[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not))] | length')
|
||||||
|
|
||||||
|
echo "🔄 [$((COUNT * SLEEP_INTERVAL))s / $((MAX_RETRY * SLEEP_INTERVAL))s] Container baru running: ${NEW_RUNNING} | Gagal: ${FAILED}"
|
||||||
|
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status) | id: \(.Id[:12])"'
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt "0" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ Ada container yang crash!"
|
||||||
|
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not)) | " → \(.Names[0]) | \(.Status)"'
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NEW_RUNNING" -gt "0" ]; then
|
||||||
|
# Cleanup dangling images setelah redeploy sukses
|
||||||
|
echo "🧹 Membersihkan dangling images..."
|
||||||
|
curl -s -X POST "https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/images/prune" \
|
||||||
|
-H "Authorization: Bearer ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"filters":{"dangling":["true"]}}' | jq -r '" Reclaimed: \(.SpaceReclaimed // 0 | . / 1073741824 | tostring | .[0:5]) GB"'
|
||||||
|
|
||||||
|
echo "✅ Cleanup selesai!"
|
||||||
|
echo ""
|
||||||
|
echo "✅ Stack $STACK_NAME berhasil di-redeploy dengan image baru dan running!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "❌ Timeout $((MAX_RETRY * SLEEP_INTERVAL))s! Container baru tidak kunjung running."
|
||||||
|
echo " Kemungkinan image masih dalam proses pull atau ada error di server."
|
||||||
|
exit 1
|
||||||
55
.github/workflows/test.yml
vendored
55
.github/workflows/test.yml
vendored
@@ -1,55 +0,0 @@
|
|||||||
name: test workflows
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
environment:
|
|
||||||
description: "Target environment (e.g., staging, production)"
|
|
||||||
required: true
|
|
||||||
default: "staging"
|
|
||||||
version:
|
|
||||||
description: "Version to deploy"
|
|
||||||
required: false
|
|
||||||
default: "latest"
|
|
||||||
|
|
||||||
env:
|
|
||||||
APP_NAME: desa-darmasaba-action
|
|
||||||
WA_PHONE: "6289697338821,6289697338822"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Checkout kode sumber
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Setup Bun
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
# Create log file
|
|
||||||
- name: Create log file
|
|
||||||
run: touch build.txt
|
|
||||||
|
|
||||||
# Step 1: Set BRANCH_NAME based on event type
|
|
||||||
- name: Set BRANCH_NAME
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
|
||||||
echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "BRANCH_NAME=${{ github.ref_name }}" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 2: Generate APP_VERSION dynamically
|
|
||||||
- name: Set APP_VERSION
|
|
||||||
run: echo "APP_VERSION=${{ github.sha }}---$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
# Step 3: Kirim notifikasi ke API build Start
|
|
||||||
- name: Notify start build
|
|
||||||
run: |
|
|
||||||
IFS=',' read -ra PHONES <<< "${{ env.WA_PHONE }}"
|
|
||||||
for PHONE in "${PHONES[@]}"; do
|
|
||||||
ENCODED_TEXT=$(bun -e "console.log(encodeURIComponent('Build:start\nApp:${{ env.APP_NAME }}\nenv:${{ inputs.environment }}\nBranch:${{ env.BRANCH_NAME }}\nVersion:${{ env.APP_VERSION }}'))")
|
|
||||||
curl -X GET "https://wa.wibudev.com/code?text=$ENCODED_TEXT&nom=$PHONE"
|
|
||||||
done
|
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -29,7 +29,12 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env
|
# env
|
||||||
|
# env local files (keep .env.example)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# QC
|
||||||
|
QC
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
@@ -47,9 +52,6 @@ next-env.d.ts
|
|||||||
# cache
|
# cache
|
||||||
/cache
|
/cache
|
||||||
|
|
||||||
.github/
|
|
||||||
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|
||||||
|
|||||||
13
.qwen/settings.json
Normal file
13
.qwen/settings.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"playwright-mcp": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"playwright-mcp@latest"
|
||||||
|
],
|
||||||
|
"timeout": 60000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$version": 3
|
||||||
|
}
|
||||||
9
.qwen/settings.json.orig
Normal file
9
.qwen/settings.json.orig
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"playwright-mcp": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "playwright-mcp@latest"],
|
||||||
|
"timeout": 60000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
AUDIT_REPORT.md
Normal file
73
AUDIT_REPORT.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Engineering Audit Report: Desa Darmasaba
|
||||||
|
**Status:** Production Readiness Review (Critical)
|
||||||
|
**Auditor:** Staff Technical Architect
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Executive Summary & Scores
|
||||||
|
|
||||||
|
| Category | Score | Status |
|
||||||
|
| :--- | :---: | :--- |
|
||||||
|
| **Project Architecture** | 3/10 | 🔴 Critical Failure |
|
||||||
|
| **Code Quality** | 4/10 | 🟠 Poor |
|
||||||
|
| **Performance** | 5/10 | 🟡 Mediocre |
|
||||||
|
| **Security** | 5/10 | 🟠 Risk Detected |
|
||||||
|
| **Production Readiness** | 2/10 | 🔴 Not Ready |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 1. Project Architecture
|
||||||
|
The project suffers from a **"Frankenstein Architecture"**. It attempts to run a full Elysia.js instance inside a Next.js Catch-All route.
|
||||||
|
- **Fractured Backend:** Logic is split between standard Next.js routes (`/api/auth`) and embedded Elysia modules.
|
||||||
|
- **Stateful Dependency:** Reliance on local filesystem (`WIBU_UPLOAD_DIR`) makes the application impossible to deploy on modern serverless platforms like Vercel.
|
||||||
|
- **Polluted Namespace:** Routing tree contains "test/coba" folders (`src/app/coba`, `src/app/percobaan`) that would be accessible in production.
|
||||||
|
|
||||||
|
## ⚛️ 2. Frontend Engineering (React / Next.js)
|
||||||
|
- **State Management Chaos:** Simultaneous use of `Valtio`, `Jotai`, `React Context`, and `localStorage`.
|
||||||
|
- **Tight Coupling:** Public pages (`/darmasaba`) import state directly from Admin internal states (`/admin/(dashboard)/_state`).
|
||||||
|
- **Heavy Client-Side Logic:** Logic that belongs in Server Actions or Hooks is embedded in presentational components (e.g., `Footer.tsx`).
|
||||||
|
|
||||||
|
## 📡 3. Backend / API Design
|
||||||
|
- **Framework Overhead:** Running Elysia inside Next.js adds unnecessary cold-boot overhead and complexity.
|
||||||
|
- **Weak Validation:** Widespread use of `as Type` casting in API handlers instead of runtime validation (Zod/Schema).
|
||||||
|
- **Service Integration:** OTP codes are sent via external `GET` requests with sensitive data in the query string—a major logging risk.
|
||||||
|
|
||||||
|
## 🗄️ 4. Database & Data Modeling (Prisma)
|
||||||
|
- **Schema Over-Normalization:** ~2000 lines of schema. Every minor content type (e.g., `LambangDesa`) is a separate table instead of a unified CMS model.
|
||||||
|
- **Polymorphic Monolith:** `FileStorage` is a "god table" with optional relations to ~40 other tables, creating a massive bottleneck and data integrity risk.
|
||||||
|
- **Connection Mismanagement:** Manual `prisma.$disconnect()` in API routes kills connection pooling performance.
|
||||||
|
|
||||||
|
## 🚀 5. Performance Engineering
|
||||||
|
- **Bypassing Optimization:** Custom `/api/utils/img` endpoint bypasses `next/image` optimization, serving uncompressed assets.
|
||||||
|
- **Aggressive Polling:** Client-side 30s polling for notifications is battery-draining and inefficient compared to SSE or SWR.
|
||||||
|
|
||||||
|
## 🔒 6. Security Audit
|
||||||
|
- **Insecure OTP Delivery:** Credentials passed as URL parameters to the WhatsApp service.
|
||||||
|
- **File Upload Risks:** Potential for Arbitrary File Upload due to direct local filesystem writes without rigorous sanitization.
|
||||||
|
|
||||||
|
## 🧹 7. Code Quality
|
||||||
|
- **Inconsistency:** Mixed English/Indonesian naming (e.g., `nomor` vs `createdAt`).
|
||||||
|
- **Artifacts:** Root directory is littered with scratch files: `xcoba.ts`, `xx.ts`, `test.txt`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚩 Top 10 Critical Problems
|
||||||
|
1. **Architectural Fracture:** Embedding Elysia inside Next.js creates a "split-brain" system.
|
||||||
|
2. **Serverless Incompatibility:** Dependency on local disk storage for uploads.
|
||||||
|
3. **Database Bloat:** Over-complicated schema with a fragile `FileStorage` monolith.
|
||||||
|
4. **State Fragmentation:** Mixed usage of Jotai and Valtio without a clear standard.
|
||||||
|
5. **Credential Leakage:** OTP codes sent via GET query parameters.
|
||||||
|
6. **Poor Cleanup:** Trial/Test folders and files committed to the production source.
|
||||||
|
7. **Asset Performance:** Bypassing Next.js image optimization.
|
||||||
|
8. **Coupling:** High dependency between public UI and internal Admin state.
|
||||||
|
9. **Type Safety:** Manual casting in APIs instead of runtime validation.
|
||||||
|
10. **Connection Pooling:** Inefficient Prisma connection management.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Tech Lead Refactoring Priorities
|
||||||
|
1. **Unify the API:** Decommission the Elysia wrapper. Port all logic to standard Next.js Route Handlers with Zod validation.
|
||||||
|
2. **Stateless Storage:** Implement an S3-compatible adapter for all file uploads. Remove `fs` usage.
|
||||||
|
3. **Schema Consolidation:** Refactor the schema to use generic content models where possible.
|
||||||
|
4. **Standardize State:** Choose one global state manager and migrate all components.
|
||||||
|
5. **Project Sanitization:** Delete all `coba`, `percobaan`, and scratch files (`xcoba.ts`, etc.).
|
||||||
191
DEV-INSPECTOR-ANALYSIS.md
Normal file
191
DEV-INSPECTOR-ANALYSIS.md
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
# Dev Inspector - Analisis & Rekomendasi untuk Project Desa Darmasaba
|
||||||
|
|
||||||
|
## 📋 Ringkasan Analisis
|
||||||
|
|
||||||
|
Dokumen `dev-inspector-click-to-source.md` **TIDAK dapat diterapkan langsung** ke project ini karena perbedaan arsitektur fundamental.
|
||||||
|
|
||||||
|
## 🔍 Perbedaan Arsitektur
|
||||||
|
|
||||||
|
| Syarat di Dokumen | Project Desa Darmasaba | Status |
|
||||||
|
|-------------------|------------------------|--------|
|
||||||
|
| **Vite sebagai bundler** | Next.js 15 (Webpack/Turbopack) | ❌ Tidak kompatibel |
|
||||||
|
| **Elysia + Vite middlewareMode** | Next.js App Router + Elysia sebagai API handler | ❌ Berbeda |
|
||||||
|
| **React** | ✅ React 19 | ✅ Kompatibel |
|
||||||
|
| **Bun runtime** | ✅ Bun | ✅ Kompatibel |
|
||||||
|
|
||||||
|
## ✅ Solusi: Next.js Sudah Punya Built-in Click-to-Source
|
||||||
|
|
||||||
|
Next.js memiliki fitur **click-to-source bawaan** yang bekerja tanpa setup tambahan:
|
||||||
|
|
||||||
|
### Cara Menggunakan
|
||||||
|
|
||||||
|
1. **Pastikan dalam development mode:**
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Klik elemen dengan modifier key:**
|
||||||
|
- **macOS**: `Option` + `Click` (atau `⌥` + `Click`)
|
||||||
|
- **Windows/Linux**: `Alt` + `Click`
|
||||||
|
|
||||||
|
3. **File akan terbuka di editor** pada baris dan kolom yang tepat
|
||||||
|
|
||||||
|
### Syarat Agar Berfungsi
|
||||||
|
|
||||||
|
1. **Editor harus ada di PATH**
|
||||||
|
|
||||||
|
VS Code biasanya sudah terdaftar. Jika menggunakan editor lain, set:
|
||||||
|
```bash
|
||||||
|
# Untuk Cursor
|
||||||
|
export EDITOR=cursor
|
||||||
|
|
||||||
|
# Untuk Windsurf
|
||||||
|
export EDITOR=windsurf
|
||||||
|
|
||||||
|
# Untuk Sublime Text
|
||||||
|
export EDITOR=subl
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Hanya berfungsi di development mode**
|
||||||
|
- Fitur ini otomatis tree-shaken di production
|
||||||
|
- Zero overhead di production build
|
||||||
|
|
||||||
|
3. **Browser DevTools harus terbuka** (beberapa browser memerlukan ini)
|
||||||
|
|
||||||
|
## 🎯 Rekomendasi untuk Project Ini
|
||||||
|
|
||||||
|
### Opsi 1: Gunakan Built-in Next.js (DIREKOMENDASIKAN)
|
||||||
|
|
||||||
|
**Kelebihan:**
|
||||||
|
- ✅ Zero setup
|
||||||
|
- ✅ Maintain oleh Vercel
|
||||||
|
- ✅ Otomatis compatible dengan Next.js updates
|
||||||
|
- ✅ Zero production overhead
|
||||||
|
|
||||||
|
**Kekurangan:**
|
||||||
|
- ⚠️ Hotkey berbeda (`Option+Click` vs `Ctrl+Shift+Cmd+C`)
|
||||||
|
- ⚠️ Tidak ada visual overlay/tooltip seperti di dokumen
|
||||||
|
|
||||||
|
**Cara:**
|
||||||
|
Tidak perlu melakukan apapun - fitur sudah aktif saat `bun run dev`.
|
||||||
|
|
||||||
|
### Opsi 2: Custom Implementation (JIKA DIPERLUKAN)
|
||||||
|
|
||||||
|
Jika ingin visual overlay dan tooltip seperti di dokumen, bisa dibuat custom component dengan pendekatan berbeda:
|
||||||
|
|
||||||
|
#### Arsitektur Alternatif untuk Next.js
|
||||||
|
|
||||||
|
```
|
||||||
|
BUILD TIME (Next.js/Webpack):
|
||||||
|
.tsx/.jsx file
|
||||||
|
→ [Custom Webpack Loader] inject data-inspector-* attributes
|
||||||
|
→ [Next.js internal transform] JSX to React.createElement
|
||||||
|
→ Browser menerima elemen dengan attributes
|
||||||
|
|
||||||
|
RUNTIME (Browser):
|
||||||
|
[SAMA seperti dokumen - DevInspector component]
|
||||||
|
|
||||||
|
BACKEND (Next.js API Route):
|
||||||
|
/__open-in-editor → Bun.spawn([editor, '--goto', 'file:line:col'])
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Komponen yang Dibutuhkan:
|
||||||
|
|
||||||
|
1. **Custom Webpack Loader** (bukan Vite Plugin)
|
||||||
|
- Inject attributes via webpack transform
|
||||||
|
- Taruh di `next.config.ts` webpack config
|
||||||
|
|
||||||
|
2. **DevInspector Component** (sama seperti dokumen)
|
||||||
|
- Browser runtime untuk handle hotkey & klik
|
||||||
|
|
||||||
|
3. **API Route `/__open-in-editor`**
|
||||||
|
- Buat sebagai Next.js API route: `src/app/api/__open-in-editor/route.ts`
|
||||||
|
- HARUS bypass auth middleware
|
||||||
|
|
||||||
|
4. **Conditional Import** (sama seperti dokumen)
|
||||||
|
```tsx
|
||||||
|
const InspectorWrapper = process.env.NODE_ENV === 'development'
|
||||||
|
? (await import('./DevInspector')).DevInspector
|
||||||
|
: ({ children }) => <>{children}</>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Implementasi Steps:
|
||||||
|
|
||||||
|
Jika Anda ingin melanjutkan dengan custom implementation, berikut steps:
|
||||||
|
|
||||||
|
1. ✅ Buat `src/components/DevInspector.tsx` (copy dari dokumen)
|
||||||
|
2. ⚠️ Buat webpack loader untuk inject attributes (perlu research)
|
||||||
|
3. ✅ Buat API route `src/app/api/__open-in-editor/route.ts`
|
||||||
|
4. ✅ Wrap root layout dengan DevInspector
|
||||||
|
5. ✅ Set `REACT_EDITOR` di `.env`
|
||||||
|
|
||||||
|
**Peringatan:**
|
||||||
|
- Webpack loader lebih kompleks daripada Vite plugin
|
||||||
|
- Mungkin ada edge cases dengan Next.js internals
|
||||||
|
- Perlu maintenance ekstra saat Next.js update
|
||||||
|
|
||||||
|
## 📊 Perbandingan
|
||||||
|
|
||||||
|
| Fitur | Built-in Next.js | Custom Implementation |
|
||||||
|
|-------|------------------|----------------------|
|
||||||
|
| Setup | ✅ Zero | ⚠️ Medium |
|
||||||
|
| Visual Overlay | ❌ Tidak ada | ✅ Ada |
|
||||||
|
| Tooltip | ❌ Tidak ada | ✅ Ada |
|
||||||
|
| Hotkey | `Option+Click` | Custom (bisa disesuaikan) |
|
||||||
|
| Maintenance | ✅ Vercel | ⚠️ Manual |
|
||||||
|
| Compatibility | ✅ Guaranteed | ⚠️ Perlu testing |
|
||||||
|
| Production Impact | ✅ Zero | ✅ Zero (dengan conditional import) |
|
||||||
|
|
||||||
|
## 🎯 Kesimpulan
|
||||||
|
|
||||||
|
**Rekomendasi: Gunakan Built-in Next.js**
|
||||||
|
|
||||||
|
Alasan:
|
||||||
|
1. ✅ Sudah tersedia - tidak perlu setup
|
||||||
|
2. ✅ Lebih stabil - maintain oleh Vercel
|
||||||
|
3. ✅ Lebih simple - tidak ada custom code
|
||||||
|
4. ✅ Future-proof - otomatis update dengan Next.js
|
||||||
|
|
||||||
|
**Custom implementation hanya diperlukan jika:**
|
||||||
|
- Anda sangat membutuhkan visual overlay & tooltip
|
||||||
|
- Anda ingin hotkey yang sama persis (`Ctrl+Shift+Cmd+C`)
|
||||||
|
- Anda punya waktu untuk maintenance
|
||||||
|
|
||||||
|
## 🚀 Quick Start - Built-in Feature
|
||||||
|
|
||||||
|
Untuk menggunakan click-to-source bawaan Next.js:
|
||||||
|
|
||||||
|
1. Jalankan development server:
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Buka browser ke `http://localhost:3000`
|
||||||
|
|
||||||
|
3. Tahan `Option` (macOS) atau `Alt` (Windows/Linux)
|
||||||
|
|
||||||
|
4. Cursor akan berubah menjadi crosshair
|
||||||
|
|
||||||
|
5. Klik elemen mana pun - file akan terbuka di editor
|
||||||
|
|
||||||
|
6. **Opsional**: Set editor di `.env`:
|
||||||
|
```env
|
||||||
|
# .env.local
|
||||||
|
EDITOR=code # atau cursor, windsurf, subl
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Notes
|
||||||
|
|
||||||
|
- Fitur ini hanya aktif di development mode (`NODE_ENV=development`)
|
||||||
|
- Production build (`bun run build`) otomatis menghilangkan fitur ini
|
||||||
|
- Next.js menggunakan mekanisme yang mirip (source mapping) untuk menentukan lokasi component
|
||||||
|
- Jika editor tidak terbuka, pastikan:
|
||||||
|
- Editor sudah terinstall dan ada di PATH
|
||||||
|
- Browser DevTools terbuka (beberapa browser require ini)
|
||||||
|
- Anda menggunakan development server, bukan production
|
||||||
|
|
||||||
|
## 🔗 Referensi
|
||||||
|
|
||||||
|
- [Next.js Documentation - Launching Editor](https://nextjs.org/docs/app/api-reference/config/next-config-js/reactStrictMode)
|
||||||
|
- [React DevTools - Component Inspection](https://react.dev/learn/react-developer-tools)
|
||||||
|
- [Original Dev Inspector Document](./dev-inspector-click-to-source.md)
|
||||||
67
Dockerfile
Normal file
67
Dockerfile
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# ==============================
|
||||||
|
# Stage 1: Builder
|
||||||
|
# ==============================
|
||||||
|
FROM oven/bun:1-debian AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libc6 \
|
||||||
|
git \
|
||||||
|
openssl \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY package.json bun.lockb* ./
|
||||||
|
|
||||||
|
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||||
|
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN cp .env.example .env || true
|
||||||
|
|
||||||
|
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
|
||||||
|
RUN bunx prisma generate
|
||||||
|
|
||||||
|
# Generate API types (opsional)
|
||||||
|
RUN bun run gen:api || echo "tidak ada gen api"
|
||||||
|
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# ==============================
|
||||||
|
# Stage 2: Runner (Production)
|
||||||
|
# ==============================
|
||||||
|
FROM oven/bun:1-debian AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
openssl \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN groupadd --system --gid 1001 nodejs \
|
||||||
|
&& useradd --system --uid 1001 --gid nodejs nextjs
|
||||||
|
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/next.config.* ./
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["bun", "start"]
|
||||||
62
GEMINI.md
Normal file
62
GEMINI.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Project: Desa Darmasaba
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
The `desa-darmasaba` project is a Next.js (version 15+) application developed with TypeScript. It serves as an official platform for Desa Darmasaba (a village in Badung, Bali), offering various public services, news, and detailed village profiles.
|
||||||
|
|
||||||
|
**Key Technologies:**
|
||||||
|
|
||||||
|
* **Frontend Framework:** Next.js (v15+) with React (v19+)
|
||||||
|
* **Language:** TypeScript
|
||||||
|
* **UI Library:** Mantine UI
|
||||||
|
* **Database ORM:** Prisma (v6+)
|
||||||
|
* **Database:** PostgreSQL (as configured in `prisma/schema.prisma`)
|
||||||
|
* **API Framework:** Elysia (used for API routes, as seen in dependencies)
|
||||||
|
* **State Management:** Potentially Jotai and Valtio (listed in dependencies)
|
||||||
|
* **Image Processing:** Sharp
|
||||||
|
* **Package Manager:** Likely Bun, given `bun.lockb` and the `prisma:seed` script.
|
||||||
|
|
||||||
|
The application architecture follows the Next.js App Router structure, with comprehensive data models defined in `prisma/schema.prisma` covering various domains like public information, health, security, economy, innovation, environment, and education. It also includes configurations for image handling and caching.
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
This project uses `bun` as the package manager. Ensure Bun is installed to run these commands.
|
||||||
|
|
||||||
|
* **Install Dependencies:**
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Development Server:**
|
||||||
|
Runs the Next.js development server.
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Build for Production:**
|
||||||
|
Builds the Next.js application for production deployment.
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Start Production Server:**
|
||||||
|
Starts the Next.js application in production mode.
|
||||||
|
```bash
|
||||||
|
bun run start
|
||||||
|
```
|
||||||
|
|
||||||
|
* **Database Seeding:**
|
||||||
|
Executes the Prisma seeding script to populate the database.
|
||||||
|
```bash
|
||||||
|
bun run prisma:seed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Conventions
|
||||||
|
|
||||||
|
* **Coding Language:** TypeScript is strictly enforced.
|
||||||
|
* **Frontend Framework:** Next.js App Router for page and component structuring.
|
||||||
|
* **UI/UX:** Adherence to Mantine UI component library for consistent styling and user experience.
|
||||||
|
* **Database Interaction:** Prisma ORM is used for all database operations, with a PostgreSQL database.
|
||||||
|
* **Linting:** ESLint is configured with `next/core-web-vitals` and `next/typescript` to maintain code quality and adherence to Next.js and TypeScript best practices.
|
||||||
|
* **Styling:** PostCSS is used, with `postcss-preset-mantine` and `postcss-simple-vars` defining Mantine-specific breakpoints and other CSS variables.
|
||||||
|
* **Imports:** Absolute imports are configured using `@/*` which resolves to the `src/` directory.
|
||||||
173
MUSIK_CREATE_ANALYSIS.md
Normal file
173
MUSIK_CREATE_ANALYSIS.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Musik Desa - Create Feature Analysis
|
||||||
|
|
||||||
|
## Error Summary
|
||||||
|
**Error**: `ERR_BLOCKED_BY_CLIENT` saat create musik di staging environment
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
### 1. **CORS Configuration Issue** (Primary)
|
||||||
|
File: `src/app/api/[[...slugs]]/route.ts`
|
||||||
|
|
||||||
|
The CORS configuration has specific origins listed:
|
||||||
|
```typescript
|
||||||
|
const corsConfig = {
|
||||||
|
origin: [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:3001",
|
||||||
|
"https://cld-dkr-desa-darmasaba-stg.wibudev.com",
|
||||||
|
"https://cld-dkr-staging-desa-darmasaba.wibudev.com",
|
||||||
|
"*",
|
||||||
|
],
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: The wildcard `*` is at the end, but some browsers don't respect it when `credentials: true` is set.
|
||||||
|
|
||||||
|
### 2. **API Fetch Base URL** (Secondary)
|
||||||
|
File: `src/lib/api-fetch.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- In staging, this might still default to `http://localhost:3000`
|
||||||
|
- Mixed content (HTTPS frontend → HTTP API) gets blocked by browsers
|
||||||
|
- The `NEXT_PUBLIC_BASE_URL` environment variable might not be set in staging
|
||||||
|
|
||||||
|
### 3. **File Storage Upload Path** (Tertiary)
|
||||||
|
File: `src/app/api/[[...slugs]]/_lib/fileStorage/_lib/create.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const UPLOAD_DIR = process.env.WIBU_UPLOAD_DIR;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem**: If `WIBU_UPLOAD_DIR` is not set or points to a non-writable location, uploads will fail silently.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
### Fix 1: Update CORS Configuration
|
||||||
|
**File**: `src/app/api/[[...slugs]]/route.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Move wildcard to first position and ensure it works with credentials
|
||||||
|
const corsConfig = {
|
||||||
|
origin: [
|
||||||
|
"*", // Allow all origins (for staging flexibility)
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:3001",
|
||||||
|
"https://cld-dkr-desa-darmasaba-stg.wibudev.com",
|
||||||
|
"https://cld-dkr-staging-desa-darmasaba.wibudev.com",
|
||||||
|
"https://desa-darmasaba-stg.wibudev.com"
|
||||||
|
],
|
||||||
|
methods: ["GET", "POST", "PATCH", "DELETE", "PUT", "OPTIONS"] as HTTPMethod[],
|
||||||
|
allowedHeaders: ["Content-Type", "Authorization", "Accept"],
|
||||||
|
exposedHeaders: ["Content-Range", "X-Content-Range"],
|
||||||
|
maxAge: 86400, // 24 hours
|
||||||
|
credentials: true,
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 2: Add Environment Variable Validation
|
||||||
|
**File**: `.env.example` (update)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Application Configuration
|
||||||
|
NEXT_PUBLIC_BASE_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# For staging/production, set this to your actual domain
|
||||||
|
# NEXT_PUBLIC_BASE_URL=https://cld-dkr-desa-darmasaba-stg.wibudev.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 3: Update API Fetch to Handle Relative URLs
|
||||||
|
**File**: `src/lib/api-fetch.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AppServer } from '@/app/api/[[...slugs]]/route'
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
|
||||||
|
// Use relative URL for better deployment flexibility
|
||||||
|
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || '/'
|
||||||
|
|
||||||
|
const ApiFetch = treaty<AppServer>(BASE_URL)
|
||||||
|
|
||||||
|
export default ApiFetch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix 4: Add Error Handling in Create Page
|
||||||
|
**File**: `src/app/admin/(dashboard)/musik/create/page.tsx`
|
||||||
|
|
||||||
|
Add better error logging to diagnose issues:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// ... validation ...
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
// Upload cover image
|
||||||
|
const coverRes = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file: coverFile,
|
||||||
|
name: coverFile.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!coverRes.data?.data?.id) {
|
||||||
|
console.error('Cover upload failed:', coverRes);
|
||||||
|
return toast.error('Gagal mengunggah cover, silakan coba lagi');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... rest of the code ...
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating musik:', {
|
||||||
|
error,
|
||||||
|
message: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
});
|
||||||
|
toast.error('Terjadi kesalahan saat membuat musik');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
- [ ] Test create musik with cover image and audio file
|
||||||
|
- [ ] Verify CORS headers in browser DevTools Network tab
|
||||||
|
- [ ] Check that file uploads are saved to correct directory
|
||||||
|
|
||||||
|
### Staging Environment
|
||||||
|
- [ ] Set `NEXT_PUBLIC_BASE_URL` to staging domain
|
||||||
|
- [ ] Verify HTTPS is used for all API calls
|
||||||
|
- [ ] Check browser console for mixed content warnings
|
||||||
|
- [ ] Verify `WIBU_UPLOAD_DIR` is set and writable
|
||||||
|
- [ ] Test create musik end-to-end
|
||||||
|
|
||||||
|
## Additional Notes
|
||||||
|
|
||||||
|
### ERR_BLOCKED_BY_CLIENT Common Causes:
|
||||||
|
1. **CORS policy blocking** - Most likely cause
|
||||||
|
2. **Ad blockers** - Can block certain API endpoints
|
||||||
|
3. **Mixed content** - HTTPS page making HTTP requests
|
||||||
|
4. **Content Security Policy (CSP)** - Restrictive CSP headers
|
||||||
|
5. **Browser extensions** - Privacy/security extensions blocking requests
|
||||||
|
|
||||||
|
### Debugging Steps:
|
||||||
|
1. Open browser DevTools → Network tab
|
||||||
|
2. Try to create musik
|
||||||
|
3. Look for failed requests (red status)
|
||||||
|
4. Check the "Headers" tab for:
|
||||||
|
- Request URL (should be correct domain)
|
||||||
|
- Response headers (should have `Access-Control-Allow-Origin`)
|
||||||
|
- Status code (4xx/5xx indicates server-side issue)
|
||||||
|
5. Check browser console for CORS errors
|
||||||
|
|
||||||
|
## Recommended Next Steps
|
||||||
|
|
||||||
|
1. **Immediate**: Update CORS configuration to allow staging domain
|
||||||
|
2. **Short-term**: Add proper environment variable validation
|
||||||
|
3. **Long-term**: Implement proper error boundaries and logging
|
||||||
305
QWEN.md
305
QWEN.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Desa Darmasaba is a comprehensive Next.js 15 application designed for village management services in Badung, Bali. The platform serves as a digital hub for government services, public information, news, and community engagement for the residents of Darmasaba village.
|
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
|
### Key Technologies
|
||||||
- **Framework**: Next.js 15 with App Router
|
- **Framework**: Next.js 15 with App Router
|
||||||
@@ -10,146 +10,104 @@ Desa Darmasaba is a comprehensive Next.js 15 application designed for village ma
|
|||||||
- **Styling**: Mantine UI components with custom CSS
|
- **Styling**: Mantine UI components with custom CSS
|
||||||
- **Backend**: Elysia.js API server integrated with Next.js
|
- **Backend**: Elysia.js API server integrated with Next.js
|
||||||
- **Database**: PostgreSQL with Prisma ORM
|
- **Database**: PostgreSQL with Prisma ORM
|
||||||
- **State Management**: Jotai for global state
|
- **State Management**: Valtio for global state
|
||||||
- **Authentication**: JWT with iron-session
|
- **Authentication**: JWT with iron-session
|
||||||
- **Runtime**: Bun (instead of Node.js)
|
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
The application follows a modern full-stack architecture with:
|
The application follows a modular architecture with:
|
||||||
- Frontend built with Next.js 15 and TypeScript
|
- A main frontend built with Next.js and Mantine UI
|
||||||
- Backend API endpoints using Elysia.js
|
- An integrated Elysia.js API server for backend operations
|
||||||
- PostgreSQL database managed with Prisma ORM
|
- Prisma ORM for database interactions
|
||||||
- Mantine UI library for consistent design components
|
- File storage integration with Seafile
|
||||||
- File storage system for managing images and documents
|
- Multiple domain-specific modules (PPID, health, security, education, etc.)
|
||||||
- Comprehensive user authentication and authorization system
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
The application provides extensive functionality across multiple domains:
|
|
||||||
|
|
||||||
### Government Services
|
|
||||||
- Public service information and requests
|
|
||||||
- Administrative online services
|
|
||||||
- Community announcements and news
|
|
||||||
- Village profile and governance information
|
|
||||||
|
|
||||||
### Health Services
|
|
||||||
- Healthcare facility information
|
|
||||||
- Health programs and initiatives
|
|
||||||
- Emergency health contacts
|
|
||||||
- Health articles and education
|
|
||||||
- Posyandu (community health posts) information
|
|
||||||
|
|
||||||
### Economic Development
|
|
||||||
- Local market information
|
|
||||||
- Job listings and employment opportunities
|
|
||||||
- Village economic statistics
|
|
||||||
- Business registration and support
|
|
||||||
|
|
||||||
### Innovation & Technology
|
|
||||||
- Digital village initiatives
|
|
||||||
- Technology adoption programs
|
|
||||||
- Innovation proposal system
|
|
||||||
- Smart village features
|
|
||||||
|
|
||||||
### Education
|
|
||||||
- School information and statistics
|
|
||||||
- Scholarship programs
|
|
||||||
- Educational support services
|
|
||||||
- Library and learning resources
|
|
||||||
|
|
||||||
### Environmental Management
|
|
||||||
- Waste management systems
|
|
||||||
- Green initiatives
|
|
||||||
- Environmental conservation programs
|
|
||||||
- Community participation activities
|
|
||||||
|
|
||||||
### Security & Safety
|
|
||||||
- Emergency contacts
|
|
||||||
- Police station information
|
|
||||||
- Crime prevention measures
|
|
||||||
- Community safety programs
|
|
||||||
|
|
||||||
## Building and Running
|
## Building and Running
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Bun runtime (version specified in package.json)
|
- Node.js (with Bun runtime)
|
||||||
- PostgreSQL database
|
- PostgreSQL database
|
||||||
- Environment variables configured
|
- Seafile server for file storage
|
||||||
|
|
||||||
### Setup Commands
|
### Setup Instructions
|
||||||
```bash
|
1. Install dependencies:
|
||||||
# Install dependencies
|
```bash
|
||||||
bun install
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
# Set up environment variables
|
2. Set up environment variables in `.env.local`:
|
||||||
cp .env.example .env.local
|
```
|
||||||
# Edit .env.local with your configuration
|
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
|
||||||
|
```
|
||||||
|
|
||||||
# Database setup
|
3. Generate Prisma client:
|
||||||
bunx prisma db push
|
```bash
|
||||||
bunx prisma generate
|
bunx prisma generate
|
||||||
|
```
|
||||||
|
|
||||||
# Seed database (optional)
|
4. Push database schema:
|
||||||
bun run prisma/seed.ts
|
```bash
|
||||||
|
bunx prisma db push
|
||||||
|
```
|
||||||
|
|
||||||
# Development mode
|
5. Seed the database:
|
||||||
bun run dev
|
```bash
|
||||||
|
bun run prisma/seed.ts
|
||||||
|
```
|
||||||
|
|
||||||
# Production build
|
6. Run the development server:
|
||||||
bun run build
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
# Start production server
|
### Available Scripts
|
||||||
bun run start
|
- `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
|
||||||
|
|
||||||
### Additional Commands
|
## Development Conventions
|
||||||
```bash
|
|
||||||
# Linting (ESLint)
|
|
||||||
bunx eslint .
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
bunx tsc --noEmit
|
|
||||||
|
|
||||||
# Prisma operations
|
|
||||||
bunx prisma generate
|
|
||||||
bunx prisma db push
|
|
||||||
bunx prisma studio
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
|
### Code Structure
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── app/ # Next.js app router pages
|
├── app/ # Next.js app router pages
|
||||||
│ ├── _com/ # Common components
|
│ ├── admin/ # Admin dashboard pages
|
||||||
│ ├── admin/ # Admin panel routes
|
│ ├── api/ # API routes with Elysia.js
|
||||||
│ ├── api/ # API routes
|
│ ├── darmasaba/ # Public-facing village pages
|
||||||
│ ├── darmasaba/ # Main application pages
|
│ └── ...
|
||||||
│ ├── login/ # Authentication pages
|
├── con/ # Constants and configuration
|
||||||
│ └── ... # Other feature pages
|
├── hooks/ # React hooks
|
||||||
├── con/ # Constants and static data
|
├── lib/ # Utility functions and configurations
|
||||||
├── hooks/ # Custom React hooks
|
├── middlewares/ # Next.js middleware
|
||||||
├── lib/ # Utility functions and configurations
|
├── state/ # Global state management
|
||||||
├── middlewares/ # Middleware functions
|
├── store/ # Additional state management
|
||||||
├── state/ # Global state management
|
├── types/ # TypeScript type definitions
|
||||||
├── store/ # Additional state management
|
└── utils/ # Utility functions
|
||||||
├── types/ # TypeScript type definitions
|
|
||||||
├── utils/ # Utility functions
|
|
||||||
├── middleware.ts # Application middleware
|
|
||||||
└── schema.ts # Schema definitions
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Guidelines
|
### Import Conventions
|
||||||
|
|
||||||
### Code Style
|
|
||||||
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
||||||
- Group imports: external libraries first, then internal modules
|
- Group imports: external libraries first, then internal modules
|
||||||
- Follow consistent naming conventions:
|
- Keep import statements organized and remove unused imports
|
||||||
- Components: PascalCase (e.g., `UploadImage.tsx`)
|
|
||||||
- Files: kebab-case for utilities (e.g., `api-fetch.ts`)
|
```typescript
|
||||||
- Variables/Functions: camelCase
|
// External libraries
|
||||||
- Constants: UPPER_SNAKE_CASE
|
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
|
### TypeScript Configuration
|
||||||
- Strict mode enabled (`"strict": true`)
|
- Strict mode enabled (`"strict": true`)
|
||||||
@@ -157,12 +115,52 @@ src/
|
|||||||
- Module resolution: bundler
|
- Module resolution: bundler
|
||||||
- Path alias: `@/*` maps to `./src/*`
|
- 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
|
### Error Handling
|
||||||
- Use try-catch blocks for async operations
|
- Use try-catch blocks for async operations
|
||||||
- Implement proper error boundaries in React components
|
- Implement proper error boundaries in React components
|
||||||
- Log errors appropriately without exposing sensitive data
|
- Log errors appropriately without exposing sensitive data
|
||||||
- Use Zod for runtime validation and type safety
|
- 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
|
### Security Practices
|
||||||
- Validate all user inputs with Zod schemas
|
- Validate all user inputs with Zod schemas
|
||||||
- Use JWT tokens for authentication
|
- Use JWT tokens for authentication
|
||||||
@@ -178,42 +176,57 @@ src/
|
|||||||
- Optimize bundle size with dynamic imports
|
- Optimize bundle size with dynamic imports
|
||||||
- Use Prisma query optimization
|
- Use Prisma query optimization
|
||||||
|
|
||||||
## Database Schema
|
## Domain Modules
|
||||||
|
|
||||||
The application uses a comprehensive PostgreSQL database schema with Prisma ORM, featuring:
|
The application is organized into several domain modules:
|
||||||
|
|
||||||
- **User Management**: Complete user authentication system with roles and permissions
|
1. **PPID (Public Information Disclosure)**: Profile, structure, information requests, legal basis
|
||||||
- **Content Management**: News, announcements, and various content types
|
2. **Health**: Health facilities, programs, emergency response, disease information
|
||||||
- **Service Management**: Various government and community services
|
3. **Security**: Community security, emergency contacts, crime prevention
|
||||||
- **File Storage**: Centralized file management system
|
4. **Education**: Schools, scholarships, educational programs
|
||||||
- **Health Information**: Healthcare facilities and programs
|
5. **Economy**: Local markets, BUMDes, employment data
|
||||||
- **Economic Data**: Market information and employment data
|
6. **Environment**: Environmental data, conservation, waste management
|
||||||
- **Educational Resources**: Schools, scholarships, and educational programs
|
7. **Innovation**: Digital services, innovation programs
|
||||||
- **Environmental Data**: Sustainability and environmental management
|
8. **Culture**: Village traditions, music, cultural preservation
|
||||||
- **Security Information**: Emergency contacts and safety measures
|
|
||||||
|
|
||||||
## API Structure
|
Each module has its own section in both the admin panel and public-facing areas.
|
||||||
|
|
||||||
- Backend uses Elysia.js with TypeScript
|
## File Storage Integration
|
||||||
- API routes are in `src/app/api/[[...slugs]]/` directory
|
|
||||||
- Use treaty client for type-safe API calls
|
The application integrates with Seafile for file storage, with specific handling for:
|
||||||
- Follow RESTful conventions for endpoints
|
- Images and documents
|
||||||
- Include proper HTTP status codes and error responses
|
- 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
|
## Deployment
|
||||||
|
|
||||||
The application includes deployment scripts for staging environments, with:
|
The application includes deployment scripts in the `NOTE.md` file that outline:
|
||||||
- Automated builds using Bun
|
- Automated deployment with GitHub API integration
|
||||||
- Database migration handling
|
|
||||||
- Environment-specific configurations
|
- Environment-specific configurations
|
||||||
- PM2 process management for production
|
- PM2 process management
|
||||||
|
- Release management with versioning
|
||||||
|
|
||||||
## Important Notes
|
## Troubleshooting
|
||||||
|
|
||||||
- The application uses a custom Elysia.js server integrated with Next.js API routes
|
Common issues and solutions:
|
||||||
- Image uploads are handled through `/api/upl-img-single` endpoint
|
- **API endpoints returning 404**: Check that environment variables are properly configured
|
||||||
- Database seeding is done with Bun runtime
|
- **Database connection errors**: Verify DATABASE_URL in environment variables
|
||||||
- The app supports Indonesian locale (id_ID) for SEO and content
|
- **File upload issues**: Ensure Seafile integration is properly configured
|
||||||
- CORS is configured to allow cross-origin requests during development
|
- **Build failures**: Run `bunx prisma generate` before building
|
||||||
- Authentication is implemented with JWT tokens and iron-session
|
|
||||||
- The application has both public-facing and admin sections with different access controls
|
## Development Workflow
|
||||||
|
|
||||||
|
1. Always run type checking before committing: `bunx tsc --noEmit`
|
||||||
|
2. Run linting to catch style issues: `bun run eslint .`
|
||||||
|
3. Test database changes with `bunx prisma db push`
|
||||||
|
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
||||||
|
5. Check environment variables are properly configured
|
||||||
|
6. Verify responsive design on different screen sizes
|
||||||
30
__tests__/api/fileStorage.test.ts
Normal file
30
__tests__/api/fileStorage.test.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
|
|
||||||
|
describe('FileStorage API', () => {
|
||||||
|
it('should fetch a list of files from /api/fileStorage/findMany', async () => {
|
||||||
|
const response = await ApiFetch.api.fileStorage.findMany.get();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
|
||||||
|
const responseBody = response.data as any;
|
||||||
|
|
||||||
|
expect(responseBody.data).toBeInstanceOf(Array);
|
||||||
|
expect(responseBody.data.length).toBe(2);
|
||||||
|
expect(responseBody.data[0].name).toBe('file1.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a file using /api/fileStorage/create', async () => {
|
||||||
|
const mockFile = new File(['hello'], 'hello.png', { type: 'image/png' });
|
||||||
|
const response = await ApiFetch.api.fileStorage.create.post({
|
||||||
|
file: mockFile,
|
||||||
|
name: 'hello.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
const responseBody = response.data as any;
|
||||||
|
|
||||||
|
expect(responseBody.data.realName).toBe('hello.png');
|
||||||
|
expect(responseBody.data.id).toBe('3');
|
||||||
|
});
|
||||||
|
});
|
||||||
11
__tests__/e2e/homepage.spec.ts
Normal file
11
__tests__/e2e/homepage.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('homepage has correct title and content', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Wait for the redirect to /darmasaba
|
||||||
|
await page.waitForURL('/darmasaba');
|
||||||
|
|
||||||
|
// Check for the main heading
|
||||||
|
await expect(page.getByText('DARMASABA', { exact: true })).toBeVisible();
|
||||||
|
});
|
||||||
43
__tests__/mocks/handlers.ts
Normal file
43
__tests__/mocks/handlers.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { http, HttpResponse } from 'msw';
|
||||||
|
|
||||||
|
export const handlers = [
|
||||||
|
http.get('http://localhost:3000/api/fileStorage/findMany', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: [
|
||||||
|
{ id: '1', name: 'file1.jpg', url: '/uploads/file1.jpg' },
|
||||||
|
{ id: '2', name: 'file2.png', url: '/uploads/file2.png' },
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 2,
|
||||||
|
totalPages: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
http.post('http://localhost:3000/api/fileStorage/create', async ({ request }) => {
|
||||||
|
const data = await request.formData();
|
||||||
|
const file = data.get('file') as File;
|
||||||
|
const name = data.get('name') as string;
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return new HttpResponse(null, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: {
|
||||||
|
id: '3',
|
||||||
|
name: 'generated-nanoid',
|
||||||
|
path: `/uploads/generated-nanoid`,
|
||||||
|
link: `/uploads/generated-nanoid`,
|
||||||
|
realName: name,
|
||||||
|
mimeType: file.type,
|
||||||
|
category: "uncategorized",
|
||||||
|
isActive: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
deletedAt: null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
];
|
||||||
4
__tests__/mocks/server.ts
Normal file
4
__tests__/mocks/server.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { setupServer } from 'msw/node';
|
||||||
|
import { handlers } from './handlers';
|
||||||
|
|
||||||
|
export const server = setupServer(...handlers);
|
||||||
7
__tests__/setup.ts
Normal file
7
__tests__/setup.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { server } from './mocks/server';
|
||||||
|
import { beforeAll, afterEach, afterAll } from 'vitest';
|
||||||
|
|
||||||
|
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
|
||||||
|
afterEach(() => server.resetHandlers());
|
||||||
|
afterAll(() => server.close());
|
||||||
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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
serverExternalPackages: ['@elysiajs/static', 'elysia'],
|
||||||
experimental: {},
|
experimental: {},
|
||||||
allowedDevOrigins: [
|
allowedDevOrigins: [
|
||||||
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
"http://192.168.1.82:3000", // buat akses dari HP/device lain
|
||||||
@@ -19,7 +20,6 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
28
package.json
28
package.json
@@ -5,7 +5,11 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start"
|
"start": "next start",
|
||||||
|
"test:api": "vitest run",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test": "bun run test:api && bun run test:e2e",
|
||||||
|
"gen:api": ""
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "bun run prisma/seed.ts"
|
"seed": "bun run prisma/seed.ts"
|
||||||
@@ -30,7 +34,7 @@
|
|||||||
"@mantine/modals": "^8.3.6",
|
"@mantine/modals": "^8.3.6",
|
||||||
"@mantine/tiptap": "^7.17.4",
|
"@mantine/tiptap": "^7.17.4",
|
||||||
"@paljs/types": "^8.1.0",
|
"@paljs/types": "^8.1.0",
|
||||||
"@prisma/client": "^6.3.1",
|
"@prisma/client": "6.3.1",
|
||||||
"@tabler/icons-react": "^3.30.0",
|
"@tabler/icons-react": "^3.30.0",
|
||||||
"@tiptap/extension-highlight": "^2.11.7",
|
"@tiptap/extension-highlight": "^2.11.7",
|
||||||
"@tiptap/extension-link": "^2.11.7",
|
"@tiptap/extension-link": "^2.11.7",
|
||||||
@@ -50,6 +54,7 @@
|
|||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
|
"async-mutex": "^0.5.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"bun": "^1.2.2",
|
"bun": "^1.2.2",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
@@ -58,6 +63,7 @@
|
|||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"elysia": "^1.3.5",
|
"elysia": "^1.3.5",
|
||||||
"embla-carousel": "^8.6.0",
|
"embla-carousel": "^8.6.0",
|
||||||
@@ -65,7 +71,7 @@
|
|||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"extract-zip": "^2.0.1",
|
"extract-zip": "^2.0.1",
|
||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
"framer-motion": "^12.23.5",
|
"framer-motion": "^12.38.0",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
"iron-session": "^8.0.4",
|
"iron-session": "^8.0.4",
|
||||||
"jose": "^6.1.0",
|
"jose": "^6.1.0",
|
||||||
@@ -84,7 +90,7 @@
|
|||||||
"p-limit": "^6.2.0",
|
"p-limit": "^6.2.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
"primereact": "^10.9.6",
|
"primereact": "^10.9.6",
|
||||||
"prisma": "^6.3.1",
|
"prisma": "6.3.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-exif-orientation-img": "^0.1.5",
|
"react-exif-orientation-img": "^0.1.5",
|
||||||
@@ -95,7 +101,7 @@
|
|||||||
"react-transition-group": "^4.4.5",
|
"react-transition-group": "^4.4.5",
|
||||||
"react-zoom-pan-pinch": "^3.7.0",
|
"react-zoom-pan-pinch": "^3.7.0",
|
||||||
"readdirp": "^4.1.1",
|
"readdirp": "^4.1.1",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^3.8.0",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"swr": "^2.3.2",
|
"swr": "^2.3.2",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
@@ -105,17 +111,25 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@types/cli-progress": "^3.11.6",
|
"@types/cli-progress": "^3.11.6",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@vitest/ui": "^4.0.18",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.6",
|
"eslint-config-next": "15.5.12",
|
||||||
|
"jsdom": "^28.0.0",
|
||||||
|
"msw": "^2.12.9",
|
||||||
"parcel": "^2.6.2",
|
"parcel": "^2.6.2",
|
||||||
|
"playwright-mcp": "^0.0.19",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.1",
|
||||||
"postcss-preset-mantine": "^1.17.0",
|
"postcss-preset-mantine": "^1.17.0",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# Page snapshot
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- generic [active] [ref=e1]:
|
||||||
|
- generic [ref=e2]:
|
||||||
|
- generic [ref=e7]:
|
||||||
|
- button "Darmasaba Logo" [ref=e8] [cursor=pointer]:
|
||||||
|
- img "Darmasaba Logo" [ref=e10]
|
||||||
|
- button "PPID" [ref=e11] [cursor=pointer]:
|
||||||
|
- generic [ref=e13]: PPID
|
||||||
|
- button "Desa" [ref=e14] [cursor=pointer]:
|
||||||
|
- generic [ref=e16]: Desa
|
||||||
|
- button "Kesehatan" [ref=e17] [cursor=pointer]:
|
||||||
|
- generic [ref=e19]: Kesehatan
|
||||||
|
- button "Keamanan" [ref=e20] [cursor=pointer]:
|
||||||
|
- generic [ref=e22]: Keamanan
|
||||||
|
- button "Ekonomi" [ref=e23] [cursor=pointer]:
|
||||||
|
- generic [ref=e25]: Ekonomi
|
||||||
|
- button "Inovasi" [ref=e26] [cursor=pointer]:
|
||||||
|
- generic [ref=e28]: Inovasi
|
||||||
|
- button "Lingkungan" [ref=e29] [cursor=pointer]:
|
||||||
|
- generic [ref=e31]: Lingkungan
|
||||||
|
- button "Pendidikan" [ref=e32] [cursor=pointer]:
|
||||||
|
- generic [ref=e34]: Pendidikan
|
||||||
|
- button "Musik" [ref=e35] [cursor=pointer]:
|
||||||
|
- generic [ref=e37]: Musik
|
||||||
|
- button [ref=e38] [cursor=pointer]:
|
||||||
|
- img [ref=e40]
|
||||||
|
- generic [ref=e46]:
|
||||||
|
- generic [ref=e51]:
|
||||||
|
- generic [ref=e52]:
|
||||||
|
- generic [ref=e53]:
|
||||||
|
- img "Logo Darmasaba" [ref=e55]
|
||||||
|
- img "Logo Pudak" [ref=e57]
|
||||||
|
- generic [ref=e63]:
|
||||||
|
- generic [ref=e65]:
|
||||||
|
- generic [ref=e66]:
|
||||||
|
- img [ref=e67]
|
||||||
|
- paragraph [ref=e71]: Jam Operasional
|
||||||
|
- generic [ref=e72]:
|
||||||
|
- generic [ref=e74]: Buka
|
||||||
|
- paragraph [ref=e75]: 07:30 - 15:30
|
||||||
|
- generic [ref=e77]:
|
||||||
|
- generic [ref=e78]:
|
||||||
|
- img [ref=e79]
|
||||||
|
- paragraph [ref=e82]: Hari Ini
|
||||||
|
- generic [ref=e83]:
|
||||||
|
- paragraph [ref=e84]: Status Kantor
|
||||||
|
- paragraph [ref=e85]: Sedang Beroperasi
|
||||||
|
- paragraph [ref=e95]: Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa. Semua lebih mudah dengan fitur interaktif yang kami sediakan.
|
||||||
|
- generic [ref=e102]:
|
||||||
|
- generic [ref=e103]: Browser Anda tidak mendukung video.
|
||||||
|
- generic [ref=e106]:
|
||||||
|
- heading "Penghargaan Desa" [level=2] [ref=e107]
|
||||||
|
- paragraph [ref=e110]: Sedang memuat data penghargaan...
|
||||||
|
- button "Lihat semua penghargaan" [ref=e111] [cursor=pointer]:
|
||||||
|
- generic [ref=e112]:
|
||||||
|
- paragraph [ref=e114]: Lihat Semua Penghargaan
|
||||||
|
- img [ref=e116]
|
||||||
|
- generic [ref=e119]:
|
||||||
|
- generic [ref=e121]:
|
||||||
|
- heading "Layanan" [level=1] [ref=e122]
|
||||||
|
- paragraph [ref=e123]: Layanan adalah fitur yang membantu warga desa mengakses berbagai kebutuhan administrasi, informasi, dan bantuan secara cepat, mudah, dan transparan. Dengan fitur ini, semua layanan desa ada dalam genggaman Anda!
|
||||||
|
- link "Detail" [ref=e125] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/desa/layanan
|
||||||
|
- generic [ref=e127]: Detail
|
||||||
|
- separator [ref=e129]
|
||||||
|
- generic [ref=e130]:
|
||||||
|
- generic [ref=e131]:
|
||||||
|
- paragraph [ref=e132]: Potensi Desa
|
||||||
|
- paragraph [ref=e133]: Jelajahi berbagai potensi dan peluang yang dimiliki desa. Fitur ini membantu warga maupun pemerintah desa dalam merencanakan dan mengembangkan program berbasis kekuatan lokal.
|
||||||
|
- paragraph [ref=e136]: Sedang memuat potensi desa...
|
||||||
|
- button "Lihat Semua Potensi" [ref=e139] [cursor=pointer]:
|
||||||
|
- generic [ref=e140]:
|
||||||
|
- generic [ref=e141]: Lihat Semua Potensi
|
||||||
|
- img [ref=e143]
|
||||||
|
- separator [ref=e146]
|
||||||
|
- generic [ref=e147]:
|
||||||
|
- generic [ref=e148]:
|
||||||
|
- paragraph [ref=e150]: Desa Anti Korupsi
|
||||||
|
- paragraph [ref=e151]: Desa antikorupsi mendorong pemerintahan jujur dan transparan. Keuangan desa dikelola secara terbuka dengan melibatkan warga dalam pengawasan anggaran, sehingga digunakan tepat sasaran dan sesuai kebutuhan masyarakat.
|
||||||
|
- link "Selengkapnya" [ref=e153] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/desa-anti-korupsi/detail
|
||||||
|
- generic [ref=e155]: Selengkapnya
|
||||||
|
- paragraph [ref=e158]: Memuat Data...
|
||||||
|
- generic [ref=e166]:
|
||||||
|
- heading "SDGs Desa" [level=1] [ref=e168]
|
||||||
|
- paragraph [ref=e169]: SDGs Desa adalah upaya desa untuk menciptakan pembangunan yang maju, inklusif, dan berkelanjutan melalui 17 tujuan mulai dari pengentasan kemiskinan, pendidikan, kesehatan, hingga pelestarian lingkungan.
|
||||||
|
- generic [ref=e170]:
|
||||||
|
- generic [ref=e171]:
|
||||||
|
- img [ref=e172]
|
||||||
|
- paragraph [ref=e175]: Data SDGs Desa belum tersedia
|
||||||
|
- link "Jelajahi Semua Tujuan SDGs Desa" [ref=e177] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/sdgs-desa
|
||||||
|
- paragraph [ref=e180]: Jelajahi Semua Tujuan SDGs Desa
|
||||||
|
- generic [ref=e181]:
|
||||||
|
- generic [ref=e183]:
|
||||||
|
- heading "APBDes" [level=1] [ref=e184]
|
||||||
|
- paragraph [ref=e185]: Transparansi APBDes Darmasaba adalah langkah nyata menuju tata kelola desa yang bersih, terbuka, dan bertanggung jawab.
|
||||||
|
- link "Lihat Semua Data" [ref=e187] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/apbdes
|
||||||
|
- generic [ref=e189]: Lihat Semua Data
|
||||||
|
- generic [ref=e191]:
|
||||||
|
- paragraph [ref=e193]: Pilih Tahun APBDes
|
||||||
|
- generic [ref=e194]:
|
||||||
|
- textbox "Pilih Tahun APBDes" [ref=e195]:
|
||||||
|
- /placeholder: Pilih tahun
|
||||||
|
- generic:
|
||||||
|
- img
|
||||||
|
- paragraph [ref=e197]: Tidak ada data APBDes untuk tahun yang dipilih.
|
||||||
|
- generic [ref=e202]:
|
||||||
|
- heading "Prestasi Desa" [level=1] [ref=e203]
|
||||||
|
- paragraph [ref=e204]: Kami bangga dengan pencapaian desa hingga saat ini. Semoga prestasi ini menjadi inspirasi untuk terus berkarya dan berinovasi demi kemajuan bersama.
|
||||||
|
- link "Lihat Semua Prestasi" [ref=e205] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/prestasi-desa
|
||||||
|
- generic [ref=e207]: Lihat Semua Prestasi
|
||||||
|
- button [ref=e211] [cursor=pointer]:
|
||||||
|
- img [ref=e214]
|
||||||
|
- button [ref=e219] [cursor=pointer]:
|
||||||
|
- img [ref=e221]
|
||||||
|
- generic [ref=e225]:
|
||||||
|
- contentinfo [ref=e228]:
|
||||||
|
- generic [ref=e230]:
|
||||||
|
- generic [ref=e231]:
|
||||||
|
- heading "Komitmen Layanan Kami" [level=2] [ref=e232]
|
||||||
|
- generic [ref=e233]:
|
||||||
|
- generic [ref=e234]:
|
||||||
|
- paragraph [ref=e235]: "1. Transparansi:"
|
||||||
|
- paragraph [ref=e236]: Pengelolaan dana desa dilakukan secara terbuka agar masyarakat dapat memahami dan memantau penggunaan anggaran.
|
||||||
|
- generic [ref=e237]:
|
||||||
|
- paragraph [ref=e238]: "2. Profesionalisme:"
|
||||||
|
- paragraph [ref=e239]: Layanan desa diberikan secara cepat, adil, dan profesional demi kepuasan masyarakat.
|
||||||
|
- generic [ref=e240]:
|
||||||
|
- paragraph [ref=e241]: "3. Partisipasi:"
|
||||||
|
- paragraph [ref=e242]: Masyarakat dilibatkan aktif dalam pengambilan keputusan demi pembangunan desa yang berhasil.
|
||||||
|
- generic [ref=e243]:
|
||||||
|
- paragraph [ref=e244]: "4. Inovasi:"
|
||||||
|
- paragraph [ref=e245]: Kami terus berinovasi, termasuk melalui teknologi, agar layanan semakin mudah diakses.
|
||||||
|
- generic [ref=e246]:
|
||||||
|
- paragraph [ref=e247]: "5. Keadilan:"
|
||||||
|
- paragraph [ref=e248]: Kebijakan dan program disusun untuk memberi manfaat yang merata bagi seluruh warga.
|
||||||
|
- generic [ref=e249]:
|
||||||
|
- paragraph [ref=e250]: "6. Pemberdayaan:"
|
||||||
|
- paragraph [ref=e251]: Masyarakat didukung melalui pelatihan, pendampingan, dan pengembangan usaha lokal.
|
||||||
|
- generic [ref=e252]:
|
||||||
|
- paragraph [ref=e253]: "7. Ramah Lingkungan:"
|
||||||
|
- paragraph [ref=e254]: Seluruh kegiatan pembangunan memperhatikan keberlanjutan demi menjaga alam dan kesehatan warga.
|
||||||
|
- separator [ref=e255]
|
||||||
|
- generic [ref=e256]:
|
||||||
|
- heading "Visi Kami" [level=2] [ref=e257]
|
||||||
|
- paragraph [ref=e258]: Dengan visi ini, kami berkomitmen menjadikan desa sebagai tempat yang aman, sejahtera, dan nyaman bagi seluruh warga.
|
||||||
|
- paragraph [ref=e259]: Kami percaya kemajuan dimulai dari kerja sama antara pemerintah desa dan masyarakat, didukung tata kelola yang baik demi kepentingan bersama. Saran maupun keluhan dapat disampaikan melalui kontak di bawah ini.
|
||||||
|
- generic [ref=e260]:
|
||||||
|
- paragraph [ref=e261]: "\"Desa Kuat, Warga Sejahtera!\""
|
||||||
|
- button "Logo Desa" [ref=e262] [cursor=pointer]:
|
||||||
|
- generic [ref=e263]:
|
||||||
|
- img "Logo Desa"
|
||||||
|
- generic [ref=e265]:
|
||||||
|
- generic [ref=e267]:
|
||||||
|
- paragraph [ref=e268]: Tentang Darmasaba
|
||||||
|
- paragraph [ref=e269]: Darmasaba adalah desa budaya yang kaya akan tradisi dan nilai-nilai warisan Bali.
|
||||||
|
- generic [ref=e270]:
|
||||||
|
- link [ref=e271] [cursor=pointer]:
|
||||||
|
- /url: https://www.facebook.com/DarmasabaDesaku
|
||||||
|
- img [ref=e273]
|
||||||
|
- link [ref=e275] [cursor=pointer]:
|
||||||
|
- /url: https://www.instagram.com/ddarmasaba/
|
||||||
|
- img [ref=e277]
|
||||||
|
- link [ref=e280] [cursor=pointer]:
|
||||||
|
- /url: https://www.youtube.com/channel/UCtPw9WOQO7d2HIKzKgel4Xg
|
||||||
|
- img [ref=e282]
|
||||||
|
- link [ref=e285] [cursor=pointer]:
|
||||||
|
- /url: https://www.tiktok.com/@desa.darmasaba?is_from_webapp=1&sender_device=pc
|
||||||
|
- img [ref=e287]
|
||||||
|
- generic [ref=e290]:
|
||||||
|
- paragraph [ref=e291]: Layanan Desa
|
||||||
|
- link "Administrasi Kependudukan" [ref=e292] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/desa/layanan/
|
||||||
|
- link "Layanan Sosial" [ref=e293] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/ekonomi/program-kemiskinan
|
||||||
|
- link "Pengaduan Masyarakat" [ref=e294] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/keamanan/laporan-publik
|
||||||
|
- link "Informasi Publik" [ref=e295] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
|
||||||
|
- generic [ref=e297]:
|
||||||
|
- paragraph [ref=e298]: Tautan Penting
|
||||||
|
- link "Portal Badung" [ref=e299] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/desa/berita/semua
|
||||||
|
- link "E-Government" [ref=e300] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/inovasi/desa-digital-smart-village
|
||||||
|
- link "Transparansi" [ref=e301] [cursor=pointer]:
|
||||||
|
- /url: /darmasaba/ppid/daftar-informasi-publik-desa-darmasaba
|
||||||
|
- generic [ref=e303]:
|
||||||
|
- paragraph [ref=e304]: Berlangganan Info
|
||||||
|
- paragraph [ref=e305]: Dapatkan kabar terbaru tentang program dan kegiatan desa langsung ke email Anda.
|
||||||
|
- generic [ref=e306]:
|
||||||
|
- generic [ref=e308]:
|
||||||
|
- textbox "Masukkan email Anda" [ref=e309]
|
||||||
|
- img [ref=e311]
|
||||||
|
- button "Daftar" [ref=e314] [cursor=pointer]:
|
||||||
|
- generic [ref=e316]: Daftar
|
||||||
|
- separator [ref=e317]
|
||||||
|
- paragraph [ref=e318]: © 2025 Desa Darmasaba. Hak cipta dilindungi.
|
||||||
|
- region "Notifications Alt+T"
|
||||||
|
- button "Open Next.js Dev Tools" [ref=e324] [cursor=pointer]:
|
||||||
|
- img [ref=e325]
|
||||||
|
- alert [ref=e328]
|
||||||
|
```
|
||||||
85
playwright-report/index.html
Normal file
85
playwright-report/index.html
Normal file
File diff suppressed because one or more lines are too long
25
playwright.config.ts
Normal file
25
playwright.config.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './__tests__/e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: 'bun run dev',
|
||||||
|
url: 'http://localhost:3000',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -26,7 +26,24 @@ export async function seedBerita() {
|
|||||||
|
|
||||||
console.log("🔄 Seeding Berita...");
|
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) {
|
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;
|
let imageId: string | null = null;
|
||||||
|
|
||||||
if (b.imageName) {
|
if (b.imageName) {
|
||||||
@@ -44,26 +61,32 @@ export async function seedBerita() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.berita.upsert({
|
try {
|
||||||
where: { id: b.id },
|
await prisma.berita.upsert({
|
||||||
update: {
|
where: { id: b.id },
|
||||||
judul: b.judul,
|
update: {
|
||||||
deskripsi: b.deskripsi,
|
judul: b.judul,
|
||||||
content: b.content,
|
deskripsi: b.deskripsi,
|
||||||
kategoriBeritaId: b.kategoriBeritaId,
|
content: b.content,
|
||||||
imageId,
|
kategoriBeritaId: b.kategoriBeritaId,
|
||||||
},
|
imageId,
|
||||||
create: {
|
},
|
||||||
id: b.id,
|
create: {
|
||||||
judul: b.judul,
|
id: b.id,
|
||||||
deskripsi: b.deskripsi,
|
judul: b.judul,
|
||||||
content: b.content,
|
deskripsi: b.deskripsi,
|
||||||
kategoriBeritaId: b.kategoriBeritaId,
|
content: b.content,
|
||||||
imageId,
|
kategoriBeritaId: b.kategoriBeritaId,
|
||||||
},
|
imageId,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`✅ Berita seeded: ${b.judul}`);
|
console.log(`✅ Berita seeded: ${b.judul}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(
|
||||||
|
`❌ Failed to seed berita "${b.judul}": ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("🎉 Berita seed selesai");
|
console.log("🎉 Berita seed selesai");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import maskotDesa from "../../../data/desa/profile/maskot_desa.json";
|
|||||||
import profilePerbekel from "../../../data/desa/profile/profil_perbekel.json";
|
import profilePerbekel from "../../../data/desa/profile/profil_perbekel.json";
|
||||||
import profileDesaImage from "../../../data/desa/profile/profileDesaImage.json";
|
import profileDesaImage from "../../../data/desa/profile/profileDesaImage.json";
|
||||||
import sejarahDesa from "../../../data/desa/profile/sejarah_desa.json";
|
import sejarahDesa from "../../../data/desa/profile/sejarah_desa.json";
|
||||||
|
import visiMisiDesa from "../../../data/desa/profile/visi_misi_desa.json";
|
||||||
|
|
||||||
export async function seedProfileDesa() {
|
export async function seedProfileDesa() {
|
||||||
// =========== SEJARAH DESA ===========
|
// =========== SEJARAH DESA ===========
|
||||||
@@ -26,6 +27,26 @@ export async function seedProfileDesa() {
|
|||||||
|
|
||||||
console.log("sejarah desa success ...");
|
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 ===========
|
// =========== MASKOT DESA ===========
|
||||||
for (const l of maskotDesa) {
|
for (const l of maskotDesa) {
|
||||||
await prisma.maskotDesa.upsert({
|
await prisma.maskotDesa.upsert({
|
||||||
|
|||||||
25
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
25
prisma/_seeder_list/ekonomi/seed_demografi_pekerjaan.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import demografiPekerjaan from "../../data/ekonomi/demografi-pekerjaan/demografi-pekerjaan.json";
|
||||||
|
|
||||||
|
export async function seedDemografiPekerjaan() {
|
||||||
|
console.log("🔄 Seeding Demografi Pekerjaan...");
|
||||||
|
for (const k of demografiPekerjaan) {
|
||||||
|
await prisma.dataDemografiPekerjaan.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
pekerjaan: k.pekerjaan,
|
||||||
|
lakiLaki: k.lakiLaki,
|
||||||
|
perempuan: k.perempuan,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
pekerjaan: k.pekerjaan,
|
||||||
|
lakiLaki: k.lakiLaki,
|
||||||
|
perempuan: k.perempuan,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Demografi Pekerjaan seeded successfully");
|
||||||
|
}
|
||||||
23
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
23
prisma/_seeder_list/ekonomi/seed_jumlah_penduduk_miskin.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jumlahPendudukMiskin from "../../data/ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json";
|
||||||
|
|
||||||
|
export async function seedJumlahPendudukMiskin() {
|
||||||
|
console.log("🔄 Seeding Jumlah Penduduk Miskin...");
|
||||||
|
for (const k of jumlahPendudukMiskin) {
|
||||||
|
await prisma.grafikJumlahPendudukMiskin.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
year: k.year,
|
||||||
|
totalPoorPopulation: k.totalPoorPopulation,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
year: k.year,
|
||||||
|
totalPoorPopulation: k.totalPoorPopulation,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Jumlah Penduduk Miskin seeded successfully");
|
||||||
|
}
|
||||||
27
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
27
prisma/_seeder_list/ekonomi/seed_jumlah_pengangguran.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jumlahPengangguran from "../../data/ekonomi/jumlah-pengangguran/detail-data-pengangguran.json";
|
||||||
|
|
||||||
|
export async function seedJumlahPengangguran() {
|
||||||
|
for (const d of jumlahPengangguran) {
|
||||||
|
await prisma.detailDataPengangguran.upsert({
|
||||||
|
where: {
|
||||||
|
month_year: { month: d.month, year: d.year },
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
totalUnemployment: d.totalUnemployment,
|
||||||
|
educatedUnemployment: d.educatedUnemployment,
|
||||||
|
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||||
|
percentageChange: d.percentageChange,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
month: d.month,
|
||||||
|
year: d.year,
|
||||||
|
totalUnemployment: d.totalUnemployment,
|
||||||
|
educatedUnemployment: d.educatedUnemployment,
|
||||||
|
uneducatedUnemployment: d.uneducatedUnemployment,
|
||||||
|
percentageChange: d.percentageChange,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("📊 detailDataPengangguran success ...");
|
||||||
|
}
|
||||||
35
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
35
prisma/_seeder_list/ekonomi/seed_lowongan_kerja_lokal.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import lowonganKerjaLokal from "../../data/ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json";
|
||||||
|
|
||||||
|
export async function seedLowonganKerjaLokal() {
|
||||||
|
console.log("🔄 Seeding Lowongan Kerja Lokal...");
|
||||||
|
for (const k of lowonganKerjaLokal) {
|
||||||
|
await prisma.lowonganPekerjaan.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
posisi: k.posisi,
|
||||||
|
namaPerusahaan: k.namaPerusahaan,
|
||||||
|
lokasi: k.lokasi,
|
||||||
|
tipePekerjaan: k.tipePekerjaan,
|
||||||
|
gaji: k.gaji,
|
||||||
|
deskripsi: k.deskripsi,
|
||||||
|
kualifikasi: k.kualifikasi,
|
||||||
|
notelp: k.notelp,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
posisi: k.posisi,
|
||||||
|
namaPerusahaan: k.namaPerusahaan,
|
||||||
|
lokasi: k.lokasi,
|
||||||
|
tipePekerjaan: k.tipePekerjaan,
|
||||||
|
gaji: k.gaji,
|
||||||
|
deskripsi: k.deskripsi,
|
||||||
|
kualifikasi: k.kualifikasi,
|
||||||
|
notelp: k.notelp,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Lowongan Kerja Lokal seeded successfully");
|
||||||
|
}
|
||||||
91
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
91
prisma/_seeder_list/ekonomi/seed_pasar_desa.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kategoriProduk from "../../data/ekonomi/pasar-desa/kategori-produk.json";
|
||||||
|
import pasarDesa from "../../data/ekonomi/pasar-desa/pasar-desa.json";
|
||||||
|
import kategoriToPasar from "../../data/ekonomi/pasar-desa/kategori-to-pasar.json";
|
||||||
|
|
||||||
|
export async function seedPasarDesa() {
|
||||||
|
console.log("🔄 Seeding Kategori Produk...");
|
||||||
|
for (const k of kategoriProduk) {
|
||||||
|
await prisma.kategoriProduk.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: k.nama,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
nama: k.nama,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Kategori Produk seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Pasar Desa...");
|
||||||
|
|
||||||
|
for (const p of pasarDesa) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for pasar desa "${p.nama}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.pasarDesa.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
nama: p.nama,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
harga: p.harga,
|
||||||
|
rating: p.rating,
|
||||||
|
alamatUsaha: p.alamatUsaha,
|
||||||
|
kontak: p.kontak,
|
||||||
|
imageId,
|
||||||
|
kategoriProdukId: p.kategoriProdukId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
nama: p.nama,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
harga: p.harga,
|
||||||
|
rating: p.rating,
|
||||||
|
alamatUsaha: p.alamatUsaha,
|
||||||
|
kontak: p.kontak,
|
||||||
|
imageId,
|
||||||
|
kategoriProdukId: p.kategoriProdukId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Pasar desa seeded: ${p.nama}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 Pasar desa seed selesai");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Kategori To Pasar...");
|
||||||
|
for (const p of kategoriToPasar) {
|
||||||
|
await prisma.kategoriToPasar.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
kategoriId: p.kategoriId,
|
||||||
|
pasarDesaId: p.pasarDesaId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
kategoriId: p.kategoriId,
|
||||||
|
pasarDesaId: p.pasarDesaId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
81
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
81
prisma/_seeder_list/ekonomi/seed_pendapatan_asli.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import apbdes from "../../data/ekonomi/pendapatan-asli-desa/apbDesa.json";
|
||||||
|
import pendapatan from "../../data/ekonomi/pendapatan-asli-desa/pendapatanDesa.json";
|
||||||
|
import belanja from "../../data/ekonomi/pendapatan-asli-desa/belanjaDesa.json";
|
||||||
|
import pembiayaan from "../../data/ekonomi/pendapatan-asli-desa/pembiayaanDesa.json";
|
||||||
|
|
||||||
|
export async function seedPendapatanAsli() {
|
||||||
|
console.log("🔄 Seeding Pendapatan Asli...");
|
||||||
|
for (const d of apbdes) {
|
||||||
|
await prisma.apbDesa.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
tahun: d.tahun,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
tahun: d.tahun,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Pendapatan Asli seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Pendapatan...");
|
||||||
|
for (const d of pendapatan) {
|
||||||
|
await prisma.pendapatan.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: d.name,
|
||||||
|
value: d.nilai
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
value: d.nilai
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Pendapatan seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Belanja...");
|
||||||
|
for (const d of belanja) {
|
||||||
|
await prisma.belanja.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: d.name,
|
||||||
|
value: d.nilai
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
value: d.nilai
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Belanja seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Pembiayaan...");
|
||||||
|
for (const d of pembiayaan) {
|
||||||
|
await prisma.pembiayaan.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: d.name,
|
||||||
|
value: d.nilai
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
value: d.nilai
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Pembiayaan seeded successfully");
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import grafikMenganggurBerdasarkanUsia from "../../data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json";
|
||||||
|
import grafikMenganggurBerdasarkanPendidikan from "../../data/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json";
|
||||||
|
|
||||||
|
export async function seedPendudukUsiaKerjaYangMenganggur() {
|
||||||
|
for (const p of grafikMenganggurBerdasarkanUsia) {
|
||||||
|
await prisma.grafikMenganggurBerdasarkanUsia.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
usia18_25: p.usia18_25,
|
||||||
|
usia26_35: p.usia26_35,
|
||||||
|
usia36_45: p.usia36_45,
|
||||||
|
usia46_keatas: p.usia46_keatas,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
usia18_25: p.usia18_25,
|
||||||
|
usia26_35: p.usia26_35,
|
||||||
|
usia36_45: p.usia36_45,
|
||||||
|
usia46_keatas: p.usia46_keatas,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
||||||
|
for (const p of grafikMenganggurBerdasarkanPendidikan) {
|
||||||
|
await prisma.grafikMenganggurBerdasarkanPendidikan.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
SD: p.SD,
|
||||||
|
SMP: p.SMP,
|
||||||
|
SMA: p.SMA,
|
||||||
|
D3: p.D3,
|
||||||
|
S1: p.S1,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
SD: p.SD,
|
||||||
|
SMP: p.SMP,
|
||||||
|
SMA: p.SMA,
|
||||||
|
D3: p.D3,
|
||||||
|
S1: p.S1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
||||||
|
}
|
||||||
50
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
50
prisma/_seeder_list/ekonomi/seed_program_kemiskinan.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programKemiskinan from "../../data/ekonomi/program-kemiskinan/program-kemiskinan.json";
|
||||||
|
import statistikKemiskinan from "../../data/ekonomi/program-kemiskinan/statistik-kemiskinan.json";
|
||||||
|
|
||||||
|
export async function seedProgramKemiskinan() {
|
||||||
|
for (const s of statistikKemiskinan) {
|
||||||
|
await prisma.statistikKemiskinan.upsert({
|
||||||
|
where: { tahun: s.tahun }, // ✅ FIX
|
||||||
|
update: {
|
||||||
|
jumlah: s.jumlah,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: s.id, // id boleh tetap
|
||||||
|
tahun: s.tahun,
|
||||||
|
jumlah: s.jumlah,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("📊 Statistik Kemiskinan seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Program Kemiskinan...");
|
||||||
|
for (const k of programKemiskinan) {
|
||||||
|
await prisma.programKemiskinan.upsert({
|
||||||
|
where: { id: k.id },
|
||||||
|
update: {
|
||||||
|
nama: k.nama,
|
||||||
|
deskripsi: k.deskripsi,
|
||||||
|
icon: k.icon,
|
||||||
|
statistik: {
|
||||||
|
connect: {
|
||||||
|
tahun: k.tahun, // 👈 BUKAN ID
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
nama: k.nama,
|
||||||
|
deskripsi: k.deskripsi,
|
||||||
|
icon: k.icon,
|
||||||
|
statistik: {
|
||||||
|
connect: {
|
||||||
|
tahun: k.tahun,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Program Kemiskinan seeded successfully");
|
||||||
|
}
|
||||||
25
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
25
prisma/_seeder_list/ekonomi/seed_sektor_unggulan_desa.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import sektorUnggulanDesa from "../../data/ekonomi/sektor-unggulan/sektor-unggulan.json";
|
||||||
|
|
||||||
|
export async function seedSektorUnggulanDesa() {
|
||||||
|
console.log("🔄 Seeding Sektor Unggulan Desa...");
|
||||||
|
for (const k of sektorUnggulanDesa) {
|
||||||
|
await prisma.sektorUnggulanDesa.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: k.name,
|
||||||
|
description: k.description,
|
||||||
|
value: k.value,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
name: k.name,
|
||||||
|
description: k.description,
|
||||||
|
value: k.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Sektor Unggulan Desa seeded successfully");
|
||||||
|
}
|
||||||
58
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
58
prisma/_seeder_list/ekonomi/seed_struktur_bumdes.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import posisiOrganisasiBumDes from "../../data/ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json";
|
||||||
|
import pegawai from "../../data/ekonomi/struktur-organisasi/pegawai-bumdes.json";
|
||||||
|
|
||||||
|
export async function seedStrukturBumdes() {
|
||||||
|
const flattenedPosisi = posisiOrganisasiBumDes.flat();
|
||||||
|
|
||||||
|
// ✅ Urutkan berdasarkan hierarki
|
||||||
|
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
|
||||||
|
|
||||||
|
for (const p of sortedPosisi) {
|
||||||
|
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
||||||
|
if (p.parentId) {
|
||||||
|
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
|
||||||
|
if (!parentExists) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await prisma.posisiOrganisasiBumDes.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: p,
|
||||||
|
create: p,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("posisi organisasi berhasil");
|
||||||
|
for (const p of pegawai) {
|
||||||
|
await prisma.pegawaiBumDes.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
namaLengkap: p.namaLengkap,
|
||||||
|
gelarAkademik: p.gelarAkademik,
|
||||||
|
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||||
|
email: p.email,
|
||||||
|
telepon: p.telepon,
|
||||||
|
alamat: p.alamat,
|
||||||
|
posisiId: p.posisiId,
|
||||||
|
isActive: p.isActive,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
namaLengkap: p.namaLengkap,
|
||||||
|
gelarAkademik: p.gelarAkademik,
|
||||||
|
tanggalMasuk: new Date(p.tanggalMasuk),
|
||||||
|
email: p.email,
|
||||||
|
telepon: p.telepon,
|
||||||
|
alamat: p.alamat,
|
||||||
|
posisiId: p.posisiId,
|
||||||
|
isActive: p.isActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("pegawai success ...");
|
||||||
|
}
|
||||||
31
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
31
prisma/_seeder_list/inovasi/seed_ajukan.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import ajukanIde from "../../data/inovasi/ajukan-ide/ajukan-ide.json";
|
||||||
|
|
||||||
|
export async function seedAjukan() {
|
||||||
|
console.log("🔄 Seeding Ajukan Ide Inovatif...");
|
||||||
|
for (const d of ajukanIde) {
|
||||||
|
await prisma.ajukanIdeInovatif.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: d.name,
|
||||||
|
alamat: d.alamat,
|
||||||
|
namaIde: d.namaIde,
|
||||||
|
deskripsi: d.deskripsi,
|
||||||
|
masalah: d.masalah,
|
||||||
|
benefit: d.benefit,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
alamat: d.alamat,
|
||||||
|
namaIde: d.namaIde,
|
||||||
|
deskripsi: d.deskripsi,
|
||||||
|
masalah: d.masalah,
|
||||||
|
benefit: d.benefit,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Ajukan Ide Inovatif seeded successfully");
|
||||||
|
}
|
||||||
42
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
42
prisma/_seeder_list/inovasi/seed_desa_digital.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import desaDigital from "../../data/inovasi/desa-digital/desa-digital.json";
|
||||||
|
|
||||||
|
export async function seedDesaDigital() {
|
||||||
|
console.log("🔄 Seeding Desa Digital...");
|
||||||
|
for (const d of desaDigital) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (d.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: d.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for desa digital "${d.name}": ${d.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.desaDigital.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: d.name,
|
||||||
|
deskripsi: d.deskripsi,
|
||||||
|
imageId: imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
deskripsi: d.deskripsi,
|
||||||
|
imageId: imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Desa Digital seeded successfully");
|
||||||
|
}
|
||||||
42
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
42
prisma/_seeder_list/inovasi/seed_info_teknologi.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import infoTeknologi from "../../data/inovasi/info-teknologi/info-teknologi.json";
|
||||||
|
|
||||||
|
export async function seedInfoTeknologi() {
|
||||||
|
console.log("🔄 Seeding Info Teknologi...");
|
||||||
|
for (const p of infoTeknologi) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for berita "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.infoTekno.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId: imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId: imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Info Teknologi seeded successfully");
|
||||||
|
}
|
||||||
66
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
66
prisma/_seeder_list/inovasi/seed_kolaborasi_inovasi.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kolaborasiInovasi from "../../data/inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json";
|
||||||
|
import mitraKolaborasi from "../../data/inovasi/kolaborasi-inovasi/mitra-kolaborasi.json";
|
||||||
|
|
||||||
|
export async function seedKolaborasiInovasi() {
|
||||||
|
console.log("🔄 Seeding Kolaborasi Inovasi...");
|
||||||
|
for (const p of kolaborasiInovasi) {
|
||||||
|
await prisma.kolaborasiInovasi.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
tahun: p.tahun,
|
||||||
|
slug: p.slug,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
kolaborator: p.kolaborator,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
tahun: p.tahun,
|
||||||
|
slug: p.slug,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
kolaborator: p.kolaborator,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Kolaborasi Inovasi seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Mitra Kolaborasi...");
|
||||||
|
for (const p of mitraKolaborasi) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for mitra kolaborasi "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.mitraKolaborasi.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
imageId: imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
imageId: imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Mitra Kolaborasi seeded successfully");
|
||||||
|
}
|
||||||
113
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
113
prisma/_seeder_list/inovasi/seed_layanan_online_desa.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jenisLayanan from "../../data/inovasi/layanan-online-desa/jenis-layanan.json";
|
||||||
|
import administrasiOnline from "../../data/inovasi/layanan-online-desa/administrasi-online.json";
|
||||||
|
import jenisPengaduan from "../../data/inovasi/layanan-online-desa/jenis-pengaduan.json";
|
||||||
|
import pengaduanMasyarakat from "../../data/inovasi/layanan-online-desa/pengaduan-masyarakat.json";
|
||||||
|
|
||||||
|
export async function seedLayananOnlineDesa() {
|
||||||
|
console.log("🔄 Seeding Jenis Layanan...");
|
||||||
|
for (const j of jenisLayanan) {
|
||||||
|
await prisma.jenisLayanan.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
deskripsi: j.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
deskripsi: j.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Jenis Layanan seeded successfully");
|
||||||
|
console.log("🔄 Seeding Administrasi Online...");
|
||||||
|
for (const d of administrasiOnline) {
|
||||||
|
await prisma.administrasiOnline.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: d.name,
|
||||||
|
alamat: d.alamat,
|
||||||
|
nomorTelepon: d.nomorTelepon,
|
||||||
|
jenisLayananId: d.jenisLayananId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
alamat: d.alamat,
|
||||||
|
nomorTelepon: d.nomorTelepon,
|
||||||
|
jenisLayananId: d.jenisLayananId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Administrasi Online seeded successfully");
|
||||||
|
console.log("🔄 Seeding Jenis Pengaduan Masyarakat...");
|
||||||
|
for (const d of jenisPengaduan) {
|
||||||
|
await prisma.jenisPengaduan.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: d.nama,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
nama: d.nama,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Jenis Pengaduan Masyarakat seeded successfully");
|
||||||
|
console.log("🔄 Seeding Pengaduan Masyarakat...");
|
||||||
|
for (const d of pengaduanMasyarakat) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (d.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: d.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for pengaduan masyarakat "${d.name}": ${d.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.pengaduanMasyarakat.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: d.name,
|
||||||
|
email: d.email,
|
||||||
|
nik: d.nik,
|
||||||
|
nomorTelepon: d.nomorTelepon,
|
||||||
|
judulPengaduan: d.judulPengaduan,
|
||||||
|
lokasiKejadian: d.lokasiKejadian,
|
||||||
|
imageId: imageId,
|
||||||
|
deskripsiPengaduan: d.deskripsiPengaduan,
|
||||||
|
jenisPengaduanId: d.jenisPengaduanId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
email: d.email,
|
||||||
|
nik: d.nik,
|
||||||
|
nomorTelepon: d.nomorTelepon,
|
||||||
|
judulPengaduan: d.judulPengaduan,
|
||||||
|
lokasiKejadian: d.lokasiKejadian,
|
||||||
|
imageId: imageId,
|
||||||
|
deskripsiPengaduan: d.deskripsiPengaduan,
|
||||||
|
jenisPengaduanId: d.jenisPengaduanId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Pengaduan Masyarakat seeded successfully");
|
||||||
|
}
|
||||||
27
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
27
prisma/_seeder_list/inovasi/seed_program_kreatif_desa.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programKreatif from "../../data/inovasi/program-kreatif-desa/program-kreatif-desa.json";
|
||||||
|
|
||||||
|
export async function seedProgramKreatifDesa() {
|
||||||
|
console.log("🔄 Seeding Program Kreatif...");
|
||||||
|
for (const p of programKreatif) {
|
||||||
|
await prisma.programKreatif.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
slug: p.slug,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
slug: p.slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Program Kreatif seeded successfully");
|
||||||
|
}
|
||||||
44
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
44
prisma/_seeder_list/keamanan/seed_keamanan_lingkungan.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import keamananLingkunganJson from "../../data/keamanan/keamanan-lingkungan/keamanan-lingkungan.json";
|
||||||
|
|
||||||
|
export async function seedKeamananLingkungan() {
|
||||||
|
console.log("🔄 Seeding Keamanan Lingkungan...");
|
||||||
|
|
||||||
|
for (const p of keamananLingkunganJson) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for keamanan lingkungan "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.keamananLingkungan.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Keamanan lingkungan seeded: ${p.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 Keamanan lingkungan seed selesai");
|
||||||
|
}
|
||||||
87
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
87
prisma/_seeder_list/keamanan/seed_kontak_darurat.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kontakDaruratKeamanan from "../../data/keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json";
|
||||||
|
import kontakItem from "../../data/keamanan/kontak-darurat-keamanan/kontakItem.json";
|
||||||
|
import kontakDaruratToItem from "../../data/keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json";
|
||||||
|
|
||||||
|
export async function seedKontakDaruratKeamanan() {
|
||||||
|
console.log("🔄 Seeding Kontak Item...");
|
||||||
|
for (const e of kontakItem) {
|
||||||
|
await prisma.kontakItem.upsert({
|
||||||
|
where: {
|
||||||
|
id: e.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: e.nama,
|
||||||
|
icon: e.icon,
|
||||||
|
nomorTelepon: e.nomorTelepon,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: e.id, // ✅ WAJIB
|
||||||
|
nama: e.nama,
|
||||||
|
icon: e.icon,
|
||||||
|
nomorTelepon: e.nomorTelepon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Kontak Item seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Kontak Darurat Keamanan...");
|
||||||
|
for (const d of kontakDaruratKeamanan) {
|
||||||
|
await prisma.kontakDaruratKeamanan.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: d.nama,
|
||||||
|
icon: d.icon,
|
||||||
|
kategoriId: d.kategoriId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
nama: d.nama,
|
||||||
|
icon: d.icon,
|
||||||
|
kategoriId: d.kategoriId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Kontak Darurat Keamanan seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Kontak Darurat To Item...");
|
||||||
|
for (const f of kontakDaruratToItem) {
|
||||||
|
// ✅ Validasi foreign keys
|
||||||
|
const kontakDaruratExists = await prisma.kontakDaruratKeamanan.findUnique({
|
||||||
|
where: { id: f.kontakDaruratId },
|
||||||
|
});
|
||||||
|
|
||||||
|
const kontakItemExists = await prisma.kontakItem.findUnique({
|
||||||
|
where: { id: f.kontakItemId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!kontakDaruratExists) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ KontakDarurat ${f.kontakDaruratId} not found, skipping...`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!kontakItemExists) {
|
||||||
|
console.warn(`⚠️ KontakItem ${f.kontakItemId} not found, skipping...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.kontakDaruratToItem.upsert({
|
||||||
|
where: { id: f.id },
|
||||||
|
update: {
|
||||||
|
kontakDaruratId: f.kontakDaruratId,
|
||||||
|
kontakItemId: f.kontakItemId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: f.id,
|
||||||
|
kontakDaruratId: f.kontakDaruratId,
|
||||||
|
kontakItemId: f.kontakItemId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Kontak Darurat To Item seeded successfully");
|
||||||
|
}
|
||||||
49
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
49
prisma/_seeder_list/keamanan/seed_laporan_publik.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import laporanPublik from "../../data/keamanan/laporan-publik/laporan-publik.json";
|
||||||
|
import penangananLaporan from "../../data/keamanan/laporan-publik/penanganan-laporan.json";
|
||||||
|
|
||||||
|
export async function seedLaporanPublik() {
|
||||||
|
console.log("🔄 Seeding Laporan Publik...");
|
||||||
|
for (const l of laporanPublik) {
|
||||||
|
await prisma.laporanPublik.upsert({
|
||||||
|
where: {
|
||||||
|
id: l.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: l.judul,
|
||||||
|
lokasi: l.lokasi,
|
||||||
|
tanggalWaktu: l.tanggalWaktu,
|
||||||
|
kronologi: l.kronologi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: l.id,
|
||||||
|
judul: l.judul,
|
||||||
|
lokasi: l.lokasi,
|
||||||
|
tanggalWaktu: l.tanggalWaktu,
|
||||||
|
kronologi: l.kronologi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("laporan publik success ...");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Penanganan Laporan...");
|
||||||
|
for (const l of penangananLaporan) {
|
||||||
|
await prisma.penangananLaporanPublik.upsert({
|
||||||
|
where: {
|
||||||
|
id: l.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
deskripsi: l.deskripsi,
|
||||||
|
laporanId: l.laporanId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: l.id,
|
||||||
|
deskripsi: l.deskripsi,
|
||||||
|
laporanId: l.laporanId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("penanganan laporan success ...");
|
||||||
|
}
|
||||||
28
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
28
prisma/_seeder_list/keamanan/seed_pencegahan_kriminalitas.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import pencegahanKriminalitas from "../../data/keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json";
|
||||||
|
|
||||||
|
export async function seedPencegahanKriminalitas() {
|
||||||
|
console.log("🔄 Seeding Pencegahan Kriminalitas...");
|
||||||
|
for (const d of pencegahanKriminalitas) {
|
||||||
|
await prisma.pencegahanKriminalitas.upsert({
|
||||||
|
where: {
|
||||||
|
id: d.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: d.judul,
|
||||||
|
deskripsi: d.deskripsi,
|
||||||
|
deskripsiSingkat: d.deskripsiSingkat,
|
||||||
|
linkVideo: d.linkVideo,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: d.id,
|
||||||
|
judul: d.judul,
|
||||||
|
deskripsi: d.deskripsi,
|
||||||
|
deskripsiSingkat: d.deskripsiSingkat,
|
||||||
|
linkVideo: d.linkVideo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Pencegahan Kriminalitas seeded successfully");
|
||||||
|
}
|
||||||
80
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
80
prisma/_seeder_list/keamanan/seed_polsek_terdekat.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import layananPolsek from "../../data/keamanan/polsek-terdekat/layanan-polsek.json";
|
||||||
|
import polsekTerdekat from "../../data/keamanan/polsek-terdekat/polsek-terdekat.json";
|
||||||
|
import layananToPolsek from "../../data/keamanan/polsek-terdekat/layanan-to-polsek.json";
|
||||||
|
|
||||||
|
export async function seedPolsekTerdekat() {
|
||||||
|
console.log("🔄 Seeding Layanan Polsek...");
|
||||||
|
for (const k of layananPolsek) {
|
||||||
|
await prisma.layananPolsek.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: k.nama,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
nama: k.nama,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("layanan polsek success ...");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Polsek Terdekat...");
|
||||||
|
for (const k of polsekTerdekat) {
|
||||||
|
await prisma.polsekTerdekat.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: k.nama,
|
||||||
|
jarakKeDesa: k.jarakKeDesa,
|
||||||
|
alamat: k.alamat,
|
||||||
|
nomorTelepon: k.nomorTelepon,
|
||||||
|
jamOperasional: k.jamOperasional,
|
||||||
|
embedMapUrl: k.embedMapUrl,
|
||||||
|
namaTempatMaps: k.namaTempatMaps,
|
||||||
|
alamatMaps: k.alamatMaps,
|
||||||
|
linkPetunjukArah: k.linkPetunjukArah,
|
||||||
|
layananPolsekId: k.layananPolsekId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
nama: k.nama,
|
||||||
|
jarakKeDesa: k.jarakKeDesa,
|
||||||
|
alamat: k.alamat,
|
||||||
|
nomorTelepon: k.nomorTelepon,
|
||||||
|
jamOperasional: k.jamOperasional,
|
||||||
|
embedMapUrl: k.embedMapUrl,
|
||||||
|
namaTempatMaps: k.namaTempatMaps,
|
||||||
|
alamatMaps: k.alamatMaps,
|
||||||
|
linkPetunjukArah: k.linkPetunjukArah,
|
||||||
|
layananPolsekId: k.layananPolsekId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("polsek terdekat success ...");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Layanan To Polsek...");
|
||||||
|
for (const k of layananToPolsek) {
|
||||||
|
await prisma.layananToPolsek.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
layananId: k.layananId,
|
||||||
|
polsekTerdekatId: k.polsekTerdekatId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
layananId: k.layananId,
|
||||||
|
polsekTerdekatId: k.polsekTerdekatId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("layanan to polsek success ...");
|
||||||
|
}
|
||||||
44
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
44
prisma/_seeder_list/keamanan/seed_tips_keamanan.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tipsKeamananJson from "../../data/keamanan/tips-keamanan/tips-keamanan.json";
|
||||||
|
|
||||||
|
export async function seedTipsKeamanan() {
|
||||||
|
console.log("🔄 Seeding Tips Keamanan...");
|
||||||
|
|
||||||
|
for (const p of tipsKeamananJson) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for tips keamanan "${p.judul}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.menuTipsKeamanan.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
judul: p.judul,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
judul: p.judul,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Tips Keamanan seeded: ${p.judul}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 Tips Keamanan seed selesai");
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import infoWabahPenyakitJson from "../../../data/kesehatan/infowabahpenyakit/infowabahpenyakit.json";
|
||||||
|
|
||||||
|
export async function seedInfoWabahPenyakit() {
|
||||||
|
console.log("🔄 Seeding Info Wabah Penyakit...");
|
||||||
|
|
||||||
|
for (const p of infoWabahPenyakitJson) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for info wabah penyakit "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.infoWabahPenyakit.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsiSingkat: p.deskripsiSingkat,
|
||||||
|
deskripsiLengkap: p.deskripsiLengkap,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsiSingkat: p.deskripsiSingkat,
|
||||||
|
deskripsiLengkap: p.deskripsiLengkap,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Info wabah penyakit seeded: ${p.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 Info wabah penyakit seed selesai");
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import kontakDaruratJson from "../../../data/kesehatan/kontak-darurat/kontak-darurat.json";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
|
||||||
|
export async function seedKontakDarurat() {
|
||||||
|
console.log("🔄 Seeding Kontak Darurat...");
|
||||||
|
|
||||||
|
for (const p of kontakDaruratJson) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for kontak darurat "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.kontakDarurat.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
whatsapp: p.whatsapp,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
whatsapp: p.whatsapp,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Kontak darurat seeded: ${p.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 Kontak darurat seed selesai");
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import penangananDaruratJson from "../../../data/kesehatan/penanganan-darurat/penganan-darurat.json";
|
||||||
|
|
||||||
|
export async function seedPenangananDarurat() {
|
||||||
|
console.log("🔄 Seeding Penanganan Darurat...");
|
||||||
|
|
||||||
|
for (const p of penangananDaruratJson) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for penanganan darurat "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.penangananDarurat.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Penanganan darurat seeded: ${p.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 Penanganan darurat seed selesai");
|
||||||
|
}
|
||||||
48
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
48
prisma/_seeder_list/kesehatan/posyandu/seed_posyandu.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import posyanduJson from "../../../data/kesehatan/posyandu/posyandu.json";
|
||||||
|
|
||||||
|
export async function seedPosyandu() {
|
||||||
|
console.log("🔄 Seeding Posyandu...");
|
||||||
|
|
||||||
|
for (const p of posyanduJson) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for posyandu "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.posyandu.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
nomor: p.nomor,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
jadwalPelayanan: p.jadwalPelayanan,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
nomor: p.nomor,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
jadwalPelayanan: p.jadwalPelayanan,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Posyandu seeded: ${p.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 Posyandu seed selesai");
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programKesehatanJson from "../../../data/kesehatan/program-kesehatan/program-kesehatan.json";
|
||||||
|
|
||||||
|
export async function seedProgramKesehatan() {
|
||||||
|
for (const p of programKesehatanJson) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for program kesehatan "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.programKesehatan.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
deskripsiSingkat: p.deskripsiSingkat,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
deskripsiSingkat: p.deskripsiSingkat,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Program kesehatan seeded: ${p.name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
95
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
95
prisma/_seeder_list/kesehatan/puskesmas/seed_puskesmas.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import puskesmasJson from "../../../data/kesehatan/puskesmas/puskesmas.json";
|
||||||
|
import kontakPuskesmasJson from "../../../data/kesehatan/puskesmas/kontak-puskesmas/kontak.json";
|
||||||
|
import jamPuskesmasJson from "../../../data/kesehatan/puskesmas/jam-puskesmas/jam.json";
|
||||||
|
|
||||||
|
export async function seedPuskesmas() {
|
||||||
|
console.log("🔄 Seeding Kontak Puskesmas...");
|
||||||
|
for (const k of kontakPuskesmasJson) {
|
||||||
|
await prisma.kontakPuskesmas.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
kontakPuskesmas: k.kontakPuskesmas,
|
||||||
|
email: k.email,
|
||||||
|
facebook: k.facebook,
|
||||||
|
kontakUGD: k.kontakUGD,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
kontakPuskesmas: k.kontakPuskesmas,
|
||||||
|
email: k.email,
|
||||||
|
facebook: k.facebook,
|
||||||
|
kontakUGD: k.kontakUGD,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("kontak puskesmas success ...");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Jam Puskesmas...");
|
||||||
|
for (const k of jamPuskesmasJson) {
|
||||||
|
await prisma.jamOperasional.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
workDays: k.workDays,
|
||||||
|
weekDays: k.weekDays,
|
||||||
|
holiday: k.holiday,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
workDays: k.workDays,
|
||||||
|
weekDays: k.weekDays,
|
||||||
|
holiday: k.holiday,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("jam puskesmas success ...");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Puskesmas...");
|
||||||
|
|
||||||
|
for (const p of puskesmasJson) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (p.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: p.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for puskesmas "${p.name}": ${p.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.puskesmas.upsert({
|
||||||
|
where: { id: p.id },
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
alamat: p.alamat,
|
||||||
|
jamId: p.jamId,
|
||||||
|
kontakId: p.kontakId,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
alamat: p.alamat,
|
||||||
|
jamId: p.jamId,
|
||||||
|
kontakId: p.kontakId,
|
||||||
|
imageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Puskesmas seeded: ${p.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎉 Puskesmas seed selesai");
|
||||||
|
}
|
||||||
|
|
||||||
71
prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts
Normal file
71
prisma/_seeder_list/lingkungan/seed_data_gotong_royong.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import kategoriGotongRoyong from "../../data/lingkungan/gotong-royong/kategori-gotong-royong.json";
|
||||||
|
import gotongRoyong from "../../data/lingkungan/gotong-royong/gotong-royong.json";
|
||||||
|
|
||||||
|
export async function seedDataGotongRoyong() {
|
||||||
|
console.log("🔄 Seeding Kategori Gotong Royong...");
|
||||||
|
|
||||||
|
for (const k of kategoriGotongRoyong) {
|
||||||
|
await prisma.kategoriKegiatan.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: k.nama,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
nama: k.nama,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Kategori Gotong Royong seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Gotong Royong...");
|
||||||
|
for (const k of gotongRoyong) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (k.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: k.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for gotong royong "${k.judul}": ${k.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.kegiatanDesa.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: k.judul,
|
||||||
|
deskripsiSingkat: k.deskripsiSingkat,
|
||||||
|
deskripsiLengkap: k.deskripsiLengkap,
|
||||||
|
tanggal: k.tanggal,
|
||||||
|
lokasi: k.lokasi,
|
||||||
|
partisipan: k.partisipan,
|
||||||
|
imageId: imageId,
|
||||||
|
kategoriKegiatanId: k.kategoriKegiatanId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
judul: k.judul,
|
||||||
|
deskripsiSingkat: k.deskripsiSingkat,
|
||||||
|
deskripsiLengkap: k.deskripsiLengkap,
|
||||||
|
tanggal: k.tanggal,
|
||||||
|
lokasi: k.lokasi,
|
||||||
|
partisipan: k.partisipan,
|
||||||
|
imageId: imageId,
|
||||||
|
kategoriKegiatanId: k.kategoriKegiatanId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Gotong Royong seeded successfully");
|
||||||
|
}
|
||||||
27
prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts
Normal file
27
prisma/_seeder_list/lingkungan/seed_data_lingkungan_desa.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import dataLingkunganDesa from "../../data/lingkungan/data-lingkungan-desa/data-lingkungan-desa.json";
|
||||||
|
|
||||||
|
export async function seedDataLingkunganDesa() {
|
||||||
|
console.log("🔄 Seeding Data Lingkungan Desa...");
|
||||||
|
for (const p of dataLingkunganDesa) {
|
||||||
|
await prisma.dataLingkunganDesa.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
jumlah: p.jumlah,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
jumlah: p.jumlah,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Data Lingkungan Desa seeded successfully");
|
||||||
|
}
|
||||||
63
prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts
Normal file
63
prisma/_seeder_list/lingkungan/seed_edukasi_lingkungan.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tujuanEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/tujuan-edukasi-lingkungan.json";
|
||||||
|
import materiEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/materi-edukasi-yang-diberikan.json";
|
||||||
|
import contohEdukasiLingkungan from "../../data/lingkungan/edukasi-lingkungan/contoh-kegiatan-di-desa-darmasaba.json";
|
||||||
|
|
||||||
|
export async function seedEdukasiLingkungan() {
|
||||||
|
for (const e of tujuanEdukasiLingkungan) {
|
||||||
|
await prisma.tujuanEdukasiLingkungan.upsert({
|
||||||
|
where: {
|
||||||
|
id: e.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: e.judul,
|
||||||
|
deskripsi: e.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: e.id,
|
||||||
|
judul: e.judul,
|
||||||
|
deskripsi: e.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("tujuan edukasi lingkungan success ...");
|
||||||
|
|
||||||
|
for (const m of materiEdukasiLingkungan) {
|
||||||
|
await prisma.materiEdukasiLingkungan.upsert({
|
||||||
|
where: {
|
||||||
|
id: m.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: m.judul,
|
||||||
|
deskripsi: m.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: m.id,
|
||||||
|
judul: m.judul,
|
||||||
|
deskripsi: m.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("materi edukasi lingkungan success ...");
|
||||||
|
|
||||||
|
for (const c of contohEdukasiLingkungan) {
|
||||||
|
await prisma.contohEdukasiLingkungan.upsert({
|
||||||
|
where: {
|
||||||
|
id: c.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: c.judul,
|
||||||
|
deskripsi: c.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: c.id,
|
||||||
|
judul: c.judul,
|
||||||
|
deskripsi: c.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("contoh edukasi lingkungan success ...");
|
||||||
|
}
|
||||||
63
prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts
Normal file
63
prisma/_seeder_list/lingkungan/seed_konservasi_adat_bali.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import filosofiTriHita from "../../data/lingkungan/konservasi-adat-bali/filosofi-tri-hita.json";
|
||||||
|
import bentukKonservasiBerdasarkanAdat from "../../data/lingkungan/konservasi-adat-bali/bentuk-konservasi.json";
|
||||||
|
import nilaiKonservasiAdat from "../../data/lingkungan/konservasi-adat-bali/nilai-konservasi-adat.json";
|
||||||
|
|
||||||
|
export async function seedKonservasiAdatBali() {
|
||||||
|
for (const f of filosofiTriHita) {
|
||||||
|
await prisma.filosofiTriHita.upsert({
|
||||||
|
where: {
|
||||||
|
id: f.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: f.judul,
|
||||||
|
deskripsi: f.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: f.id,
|
||||||
|
judul: f.judul,
|
||||||
|
deskripsi: f.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("filosofi tri hita success ...");
|
||||||
|
|
||||||
|
for (const b of bentukKonservasiBerdasarkanAdat) {
|
||||||
|
await prisma.bentukKonservasiBerdasarkanAdat.upsert({
|
||||||
|
where: {
|
||||||
|
id: b.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: b.judul,
|
||||||
|
deskripsi: b.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: b.id,
|
||||||
|
judul: b.judul,
|
||||||
|
deskripsi: b.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("bentuk konservasi berdasarkan adat success ...");
|
||||||
|
|
||||||
|
for (const n of nilaiKonservasiAdat) {
|
||||||
|
await prisma.nilaiKonservasiAdat.upsert({
|
||||||
|
where: {
|
||||||
|
id: n.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: n.judul,
|
||||||
|
deskripsi: n.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: n.id,
|
||||||
|
judul: n.judul,
|
||||||
|
deskripsi: n.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("nilai konservasi adat success ...");
|
||||||
|
}
|
||||||
51
prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts
Normal file
51
prisma/_seeder_list/lingkungan/seed_pengelolaan_sampah.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import pengelolaanSampah from "../../data/lingkungan/pengelolaan-sampah/pengelolaan-sampah.json";
|
||||||
|
import keteranganBankSampah from "../../data/lingkungan/pengelolaan-sampah/keterangan-bank-sampah.json";
|
||||||
|
|
||||||
|
export async function seedPengelolaanSampah() {
|
||||||
|
console.log("🔄 Seeding Pengelolaan Sampah...");
|
||||||
|
for (const p of pengelolaanSampah) {
|
||||||
|
await prisma.pengelolaanSampah.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Pengelolaan Sampah seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Keterangan Bank Sampah...");
|
||||||
|
for (const p of keteranganBankSampah) {
|
||||||
|
await prisma.keteranganBankSampahTerdekat.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
alamat: p.alamat,
|
||||||
|
namaTempatMaps: p.namaTempatMaps,
|
||||||
|
linkPetunjukArah: p.linkPetunjukArah,
|
||||||
|
lat: p.lat,
|
||||||
|
lng: p.lng,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
alamat: p.alamat,
|
||||||
|
namaTempatMaps: p.namaTempatMaps,
|
||||||
|
linkPetunjukArah: p.linkPetunjukArah,
|
||||||
|
lat: p.lat,
|
||||||
|
lng: p.lng,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Keterangan Bank Sampah seeded successfully");
|
||||||
|
}
|
||||||
27
prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts
Normal file
27
prisma/_seeder_list/lingkungan/seed_program_penghijauan.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import programPenghijauan from "../../data/lingkungan/program-penghijauan/program-penghijauan.json";
|
||||||
|
|
||||||
|
export async function seedProgramPenghijauan() {
|
||||||
|
console.log("🔄 Seeding Program Penghijauan...");
|
||||||
|
for (const p of programPenghijauan) {
|
||||||
|
await prisma.programPenghijauan.upsert({
|
||||||
|
where: {
|
||||||
|
id: p.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: p.name,
|
||||||
|
judul: p.judul,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
judul: p.judul,
|
||||||
|
deskripsi: p.deskripsi,
|
||||||
|
icon: p.icon,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Program Penghijauan seeded successfully");
|
||||||
|
}
|
||||||
60
prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts
Normal file
60
prisma/_seeder_list/pendidikan/seed_bimbingan_belajar.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tujuanBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/tujuan-bimbingan-belajar-desa.json";
|
||||||
|
import lokasiJadwalBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/lokasi-dan-jadwal.json";
|
||||||
|
import fasilitasBimbinganBelajarDesa from "../../data/pendidikan/bimbingan-belajar-desa/fasilitas-yang-disediakan.json";
|
||||||
|
|
||||||
|
export async function seedBimbinganBelajar() {
|
||||||
|
for (const t of tujuanBimbinganBelajarDesa) {
|
||||||
|
await prisma.tujuanBimbinganBelajarDesa.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ tujuan bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of lokasiJadwalBimbinganBelajarDesa) {
|
||||||
|
await prisma.lokasiJadwalBimbinganBelajarDesa.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ lokasi jadwal bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of fasilitasBimbinganBelajarDesa) {
|
||||||
|
await prisma.fasilitasBimbinganBelajarDesa.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
}
|
||||||
23
prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts
Normal file
23
prisma/_seeder_list/pendidikan/seed_data_pendidikan.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import dataPendidikan from "../../data/pendidikan/data-pendidikan/data-pendidikan.json";
|
||||||
|
|
||||||
|
export async function seedDataPendidikan() {
|
||||||
|
console.log("🔄 Seeding Data pendidikan...");
|
||||||
|
for (const k of dataPendidikan) {
|
||||||
|
await prisma.dataPendidikan.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: k.name,
|
||||||
|
jumlah: k.jumlah,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
name: k.name,
|
||||||
|
jumlah: k.jumlah,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Data pendidikan seeded successfully");
|
||||||
|
}
|
||||||
71
prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts
Normal file
71
prisma/_seeder_list/pendidikan/seed_data_perpustakaan.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import dataPerpustakaan from "../../data/pendidikan/perpustakaan-digital/perpustakaan-digital.json";
|
||||||
|
import kategoriBuku from "../../data/pendidikan/perpustakaan-digital/kategori-buku.json";
|
||||||
|
|
||||||
|
export async function seedDataPerpustakaan() {
|
||||||
|
console.log("🔄 Seeding Kategori Buku...");
|
||||||
|
for (const k of kategoriBuku) {
|
||||||
|
await prisma.kategoriBuku.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
name: k.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
name: k.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Kategori Buku seeded successfully");
|
||||||
|
|
||||||
|
console.log("🔄 Seeding Data perpustakaan...");
|
||||||
|
for (const k of dataPerpustakaan) {
|
||||||
|
let imageId: string | null = null;
|
||||||
|
|
||||||
|
if (k.imageName) {
|
||||||
|
const image = await prisma.fileStorage.findUnique({
|
||||||
|
where: { name: k.imageName },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
console.warn(
|
||||||
|
`⚠️ Image not found for perpustakaan "${k.judul}": ${k.imageName}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imageId = image.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.dataPerpustakaan.upsert({
|
||||||
|
where: {
|
||||||
|
id: k.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
judul: k.judul,
|
||||||
|
deskripsi: k.deskripsi,
|
||||||
|
kategoriId: k.kategoriId,
|
||||||
|
imageId: imageId
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: k.id,
|
||||||
|
judul: k.judul,
|
||||||
|
deskripsi: k.deskripsi,
|
||||||
|
kategoriId: k.kategoriId,
|
||||||
|
imageId: imageId
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Data perpustakaan seeded successfully");
|
||||||
|
}
|
||||||
|
if (import.meta.main) {
|
||||||
|
seedDataPerpustakaan()
|
||||||
|
.then(() => {
|
||||||
|
console.log("seed data perpustakaan success");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("gagal seed data perpustakaan", JSON.stringify(err));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tujuanProgram from "../../data/pendidikan/program-pendidikan-anak/tujuan-program.json";
|
||||||
|
import programUnggulan from "../../data/pendidikan/program-pendidikan-anak/program-unggulan.json";
|
||||||
|
|
||||||
|
export async function seedInfoProgramPendidikan() {
|
||||||
|
for (const t of tujuanProgram) {
|
||||||
|
await prisma.tujuanProgram.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ tujuan program seeded (editable later via UI)");
|
||||||
|
for (const t of programUnggulan) {
|
||||||
|
await prisma.programUnggulan.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ program unggulan seeded (editable later via UI)");
|
||||||
|
}
|
||||||
74
prisma/_seeder_list/pendidikan/seed_info_sekolah.ts
Normal file
74
prisma/_seeder_list/pendidikan/seed_info_sekolah.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import jenjangPendidikan from "../../data/pendidikan/info-sekolah/jenjang-pendidikan.json";
|
||||||
|
import lembagaPendidikan from "../../data/pendidikan/info-sekolah/lembaga.json";
|
||||||
|
import siswa from "../../data/pendidikan/info-sekolah/siswa.json";
|
||||||
|
import pengajar from "../../data/pendidikan/info-sekolah/pengajar.json";
|
||||||
|
|
||||||
|
export async function seedInfoSekolah() {
|
||||||
|
for (const j of jenjangPendidikan) {
|
||||||
|
await prisma.jenjangPendidikan.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Jenjang Pendidikan seeded successfully");
|
||||||
|
for (const j of lembagaPendidikan) {
|
||||||
|
await prisma.lembaga.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
jenjangId: j.jenjangId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
jenjangId: j.jenjangId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ Lembaga Pendidikan seeded successfully");
|
||||||
|
for (const j of siswa) {
|
||||||
|
await prisma.siswa.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
lembagaId: j.lembagaId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
lembagaId: j.lembagaId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ siswa seeded successfully");
|
||||||
|
for (const j of pengajar) {
|
||||||
|
await prisma.pengajar.upsert({
|
||||||
|
where: {
|
||||||
|
id: j.id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
nama: j.nama,
|
||||||
|
lembagaId: j.lembagaId,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: j.id,
|
||||||
|
nama: j.nama,
|
||||||
|
lembagaId: j.lembagaId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log("✅ pengajar seeded successfully");
|
||||||
|
}
|
||||||
60
prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts
Normal file
60
prisma/_seeder_list/pendidikan/seed_pendidikan_non_formal.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import prisma from "@/lib/prisma";
|
||||||
|
import tujuanProgram from "../../data/pendidikan/pendidikan-non-formal/tujuan-program2.json";
|
||||||
|
import tempatKegiatan from "../../data/pendidikan/pendidikan-non-formal/tempat-kegiatan.json";
|
||||||
|
import jenisProgramYangDiselenggarakan from "../../data/pendidikan/pendidikan-non-formal/jenis-program-yang-diselenggarakan.json";
|
||||||
|
|
||||||
|
export async function seedPendidikanNonFormal() {
|
||||||
|
for (const t of tujuanProgram) {
|
||||||
|
await prisma.tujuanPendidikanNonFormal.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of tempatKegiatan) {
|
||||||
|
await prisma.tempatKegiatan.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const t of jenisProgramYangDiselenggarakan) {
|
||||||
|
await prisma.jenisProgramYangDiselenggarakan.upsert({
|
||||||
|
where: { id: t.id },
|
||||||
|
update: {
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: t.id,
|
||||||
|
judul: t.judul,
|
||||||
|
deskripsi: t.deskripsi,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"✅ fasilitas bimbingan belajar desa seeded (editable later via UI)",
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"id": "1b7a17ea-83f7-4e73-a94d-96e2b4a623f2",
|
"id": "1b7a17ea-83f7-4e73-a94d-96e2b4a623f2",
|
||||||
"nama": "Warung Pasar Darmasaba",
|
"nama": "Warung Pasar Darmasaba",
|
||||||
"harga": 30000,
|
"harga": 30000,
|
||||||
"imageId": "cmkew56ls0000vnysrnzr9ttx",
|
"imageName": "YdCBnK-bWxlyHjwsk4Qie-mobile.webp",
|
||||||
"rating": 4.3,
|
"rating": 4.3,
|
||||||
"alamatUsaha": "Br. Baler Pasar, Desa Darmasaba, Kec. Abiansemal",
|
"alamatUsaha": "Br. Baler Pasar, Desa Darmasaba, Kec. Abiansemal",
|
||||||
"kontak": "081234567890",
|
"kontak": "081234567890",
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{
|
{
|
||||||
"id": "6dea2257-b710-4cd2-8d94-9b6737e658d8",
|
"id": "6dea2257-b710-4cd2-8d94-9b6737e658d8",
|
||||||
"nama": "Jajanan Pasar Bu Made",
|
"nama": "Jajanan Pasar Bu Made",
|
||||||
"imageId": "cmkewaa2s0001vnysvvs9tu56",
|
"imageName": "TWdNTZZbTOhFTNJGGPDyG-mobile.webp",
|
||||||
"harga": 5000,
|
"harga": 5000,
|
||||||
"rating": 4.6,
|
"rating": 4.6,
|
||||||
"alamatUsaha": "Jl. Raya Darmasaba, dekat Banjar Baler Pasar",
|
"alamatUsaha": "Jl. Raya Darmasaba, dekat Banjar Baler Pasar",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
{
|
{
|
||||||
"id": "24c6b992-49da-4c6e-aebb-72cf89f75438",
|
"id": "24c6b992-49da-4c6e-aebb-72cf89f75438",
|
||||||
"nama": "Sayur Segar Pak Wayan",
|
"nama": "Sayur Segar Pak Wayan",
|
||||||
"imageId": "cmkewcvfq0002vnys6985nm90",
|
"imageName": "mtQsaKtQnhxIYVIooCkiQ-mobile.webp",
|
||||||
"harga": 20000,
|
"harga": 20000,
|
||||||
"rating": 4.4,
|
"rating": 4.4,
|
||||||
"alamatUsaha": "Area Pasar Desa Darmasaba",
|
"alamatUsaha": "Area Pasar Desa Darmasaba",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
{
|
{
|
||||||
"id": "d62660a2-ac6b-428a-acf6-58cc837ef789",
|
"id": "d62660a2-ac6b-428a-acf6-58cc837ef789",
|
||||||
"nama": "Ayam & Daging Segar Darmasaba",
|
"nama": "Ayam & Daging Segar Darmasaba",
|
||||||
"imageId": "cmkewf4u90003vnys87en35nj",
|
"imageName": "Ez-SkRyf_F-1gksz_amNg-mobile.webp",
|
||||||
"harga": 80000,
|
"harga": 80000,
|
||||||
"rating": 4.2,
|
"rating": 4.2,
|
||||||
"alamatUsaha": "Br. Baler Pasar, Desa Darmasaba",
|
"alamatUsaha": "Br. Baler Pasar, Desa Darmasaba",
|
||||||
|
|||||||
@@ -3,30 +3,30 @@
|
|||||||
"id": "cmkkshcox000504l88lp54coc",
|
"id": "cmkkshcox000504l88lp54coc",
|
||||||
"name": "Darmasaba Digital App",
|
"name": "Darmasaba Digital App",
|
||||||
"deskripsi": "<p>Aplikasi digital desa yang dikembangkan oleh Pemerintah Desa Darmasaba pada tahun 2024 untuk mempermudah pelayanan publik dan informasi pemerintahan berbasis digital.</p>",
|
"deskripsi": "<p>Aplikasi digital desa yang dikembangkan oleh Pemerintah Desa Darmasaba pada tahun 2024 untuk mempermudah pelayanan publik dan informasi pemerintahan berbasis digital.</p>",
|
||||||
"imageId": "cmkksb3jr0005vni4sp3ogr87"
|
"imageName": "r_gBF0FuFpFPfSENHc4XI-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkkshln8000604l8c9b5b4il",
|
"id": "cmkkshln8000604l8c9b5b4il",
|
||||||
"name": "D’DAMART (Darmasaba Digital Market)",
|
"name": "D’DAMART (Darmasaba Digital Market)",
|
||||||
"deskripsi": "<p>Sistem pasar UMKM digital berbasis website yang dikembangkan untuk meningkatkan akses pasar dan pemasaran produk UMKM Desa Darmasaba melalui platform digital.</p>",
|
"deskripsi": "<p>Sistem pasar UMKM digital berbasis website yang dikembangkan untuk meningkatkan akses pasar dan pemasaran produk UMKM Desa Darmasaba melalui platform digital.</p>",
|
||||||
"imageId": "cmkksoze80008vni4ki2ry81r"
|
"imageName": "uE2QwpbcXyBWxVYqCWQQT-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkm1a1g80007vnsw8ejmj816",
|
"id": "cmkm1a1g80007vnsw8ejmj816",
|
||||||
"name": "Media Aspirasi dan Pengaduan Warga",
|
"name": "Media Aspirasi dan Pengaduan Warga",
|
||||||
"deskripsi": "<p>Media aspirasi dan pengaduan warga disediakan sebagai wadah partisipasi masyarakat dalam menyampaikan saran, masukan, maupun keluhan secara transparan dan terstruktur. Fitur ini memperkuat komunikasi dua arah antara pemerintah desa dan masyarakat, sehingga setiap aspirasi dapat ditindaklanjuti secara lebih cepat dan akuntabel.</p>",
|
"deskripsi": "<p>Media aspirasi dan pengaduan warga disediakan sebagai wadah partisipasi masyarakat dalam menyampaikan saran, masukan, maupun keluhan secara transparan dan terstruktur. Fitur ini memperkuat komunikasi dua arah antara pemerintah desa dan masyarakat, sehingga setiap aspirasi dapat ditindaklanjuti secara lebih cepat dan akuntabel.</p>",
|
||||||
"imageId": "cmkm1a14d0005vnsww1tsd92o"
|
"imageName": "c7xWNyoYp8Cak28NG5NoG-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkm0w0s50003vnswmwpnqsi5",
|
"id": "cmkm0w0s50003vnswmwpnqsi5",
|
||||||
"name": "Website Desa Resmi",
|
"name": "Website Desa Resmi",
|
||||||
"deskripsi": "<p>Website Desa Darmasaba berfungsi sebagai sarana utama penyampaian informasi resmi kepada masyarakat. Melalui website ini, pemerintah desa menghadirkan keterbukaan informasi publik, mempermudah akses warga terhadap berita, pengumuman, serta agenda kegiatan desa, sekaligus menjadi pusat data dan referensi terkait profil dan struktur pemerintahan desa.</p>",
|
"deskripsi": "<p>Website Desa Darmasaba berfungsi sebagai sarana utama penyampaian informasi resmi kepada masyarakat. Melalui website ini, pemerintah desa menghadirkan keterbukaan informasi publik, mempermudah akses warga terhadap berita, pengumuman, serta agenda kegiatan desa, sekaligus menjadi pusat data dan referensi terkait profil dan struktur pemerintahan desa.</p>",
|
||||||
"imageId": "cmkm0z9hx0004vnswtjd2bk3z"
|
"imageName": "kN09yF3sahmy-d5EaeGqA-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkm1c8wx000avnswksc56orq",
|
"id": "cmkm1c8wx000avnswksc56orq",
|
||||||
"name": "Publikasi Kegiatan Desa Secara Digital",
|
"name": "Publikasi Kegiatan Desa Secara Digital",
|
||||||
"deskripsi": "<p>Publikasi kegiatan desa secara digital bertujuan untuk mendokumentasikan dan menyebarluaskan berbagai aktivitas serta program kerja pemerintah desa. Melalui artikel dan dokumentasi foto, masyarakat dapat mengetahui perkembangan kegiatan desa secara terbuka, sekaligus meningkatkan kepercayaan publik terhadap pelaksanaan program desa.</p>",
|
"deskripsi": "<p>Publikasi kegiatan desa secara digital bertujuan untuk mendokumentasikan dan menyebarluaskan berbagai aktivitas serta program kerja pemerintah desa. Melalui artikel dan dokumentasi foto, masyarakat dapat mengetahui perkembangan kegiatan desa secara terbuka, sekaligus meningkatkan kepercayaan publik terhadap pelaksanaan program desa.</p>",
|
||||||
"imageId": "cmkm1c8py0008vnsw0unbxkpq"
|
"imageName": "h_Gd0SoeIJVTi_5TWUO-P-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,24 +3,24 @@
|
|||||||
"id": "cmkm2xlqr000mvnswdaymiho6",
|
"id": "cmkm2xlqr000mvnswdaymiho6",
|
||||||
"name": "Darmasaba Digital App",
|
"name": "Darmasaba Digital App",
|
||||||
"deskripsi": "<p>Aplikasi layanan desa berbasis teknologi untuk transparansi informasi dan layanan publik di Desa Darmasaba yang membantu warga mendapatkan informasi administratif, berita desa, dan pelayanan digital lainnya secara cepat dan mudah.</p>",
|
"deskripsi": "<p>Aplikasi layanan desa berbasis teknologi untuk transparansi informasi dan layanan publik di Desa Darmasaba yang membantu warga mendapatkan informasi administratif, berita desa, dan pelayanan digital lainnya secara cepat dan mudah.</p>",
|
||||||
"imageId": "cmkm3bnkt000qvnswzhqa4upf"
|
"imageName": "xVrwJgdwtcoABPU6DB__Y-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkm3b1fw000pvnswpr7hgzhp",
|
"id": "cmkm3b1fw000pvnswpr7hgzhp",
|
||||||
"name": "Program Digitalisasi Desa",
|
"name": "Program Digitalisasi Desa",
|
||||||
"deskripsi": "<p>Program kerja sama Desa Darmasaba bersama PT. Bali Interaktif Perkasa untuk memperkuat kapasitas pemanfaatan teknologi informasi dan komunikasi dalam administrasi desa, pelayanan publik, serta pemberdayaan digital masyarakat.</p>",
|
"deskripsi": "<p>Program kerja sama Desa Darmasaba bersama PT. Bali Interaktif Perkasa untuk memperkuat kapasitas pemanfaatan teknologi informasi dan komunikasi dalam administrasi desa, pelayanan publik, serta pemberdayaan digital masyarakat.</p>",
|
||||||
"imageId": "cmkm3b1a2000nvnswb9x48dzk"
|
"imageName": "JjUDrfqxuEMYSAza-s7A8-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkm3fwmq000tvnswejmhm7yc",
|
"id": "cmkm3fwmq000tvnswejmhm7yc",
|
||||||
"name": "Pengembangan Sistem Informasi Desa",
|
"name": "Pengembangan Sistem Informasi Desa",
|
||||||
"deskripsi": "<p>Inisiatif pengembangan Sistem Informasi Desa yang mendukung pengelolaan data desa secara digital, termasuk data publik, laporan, dan statistik warga, sebagai bagian dari peningkatan kapabilitas teknologi informasi desa.</p>",
|
"deskripsi": "<p>Inisiatif pengembangan Sistem Informasi Desa yang mendukung pengelolaan data desa secara digital, termasuk data publik, laporan, dan statistik warga, sebagai bagian dari peningkatan kapabilitas teknologi informasi desa.</p>",
|
||||||
"imageId": "cmkm3fwg4000rvnsw5d1vbiz0"
|
"imageName": "42RCCpBZla4ZWxXcwx7kG-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkm3hjp6000wvnswkuylnf53",
|
"id": "cmkm3hjp6000wvnswkuylnf53",
|
||||||
"name": "Pelayanan Kependudukan Berbasis Digital",
|
"name": "Pelayanan Kependudukan Berbasis Digital",
|
||||||
"deskripsi": "<p>Program untuk menyediakan layanan kependudukan secara digital, termasuk integrasi sistem administrasi kependudukan desa dengan sistem nasional, guna mempercepat layanan e-KTP, kartu keluarga, dan berkas kependudukan lainnya.</p>",
|
"deskripsi": "<p>Program untuk menyediakan layanan kependudukan secara digital, termasuk integrasi sistem administrasi kependudukan desa dengan sistem nasional, guna mempercepat layanan e-KTP, kartu keluarga, dan berkas kependudukan lainnya.</p>",
|
||||||
"imageId": "cmkm3hjhz000uvnswwqu6z9f6"
|
"imageName": "TrbkwnYM5rKZeHlISHCX4-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
{
|
{
|
||||||
"id": "cmkm1ziyi000dvnsweg8lp3f7",
|
"id": "cmkm1ziyi000dvnsweg8lp3f7",
|
||||||
"name": "TP Posyandu Bali",
|
"name": "TP Posyandu Bali",
|
||||||
"imageId": "cmkm1zis2000bvnsw85m6wdlf"
|
"imageName": "qJFWokQLCaO60j0XJU_33-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkm1ziyi000dvnsweg8lq4g8",
|
"id": "cmkm1ziyi000dvnsweg8lq4g8",
|
||||||
"name": "BRI Peduli",
|
"name": "BRI Peduli",
|
||||||
"imageId": "cmkm2dgif000evnswskk0dfo9"
|
"imageName": "nzLJoEAfl7HkpUcYa8Y1E-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkm1ziyi000dvnsweg8lr5h9",
|
"id": "cmkm1ziyi000dvnsweg8lr5h9",
|
||||||
"name": "Universitas Warmadewa (KKN-PMM)",
|
"name": "Universitas Warmadewa (KKN-PMM)",
|
||||||
"imageId": "cmkm2fzub000hvnswnvoytlzs"
|
"imageName": "JFd5C2FoaZcgDQUmvp-AO-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"deskripsiPengaduan": "<p>Permintaan Pemasangan Spanduk Larangan Bagi Hewan</p>",
|
"deskripsiPengaduan": "<p>Permintaan Pemasangan Spanduk Larangan Bagi Hewan</p>",
|
||||||
"lokasiKejadian": "Banjar Darmasaba Tengah",
|
"lokasiKejadian": "Banjar Darmasaba Tengah",
|
||||||
"jenisPengaduanId": "eommt91ma000004lb4dpq7ll1",
|
"jenisPengaduanId": "eommt91ma000004lb4dpq7ll1",
|
||||||
"imageId": "cmkkxep9l000evni4xkegbk72"
|
"imageName": "gyNi4s8TnK2UrViU-gN2C-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkkrxmub0004vni41cwyhid5",
|
"id": "cmkkrxmub0004vni41cwyhid5",
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"deskripsiPengaduan": "<p>Laporan Anjing Liar Sering Menyerang Warga</p>",
|
"deskripsiPengaduan": "<p>Laporan Anjing Liar Sering Menyerang Warga</p>",
|
||||||
"lokasiKejadian": "Jl. Raya Darmasaba",
|
"lokasiKejadian": "Jl. Raya Darmasaba",
|
||||||
"jenisPengaduanId": "eommt91ma000004lb4dpq8mm2",
|
"jenisPengaduanId": "eommt91ma000004lb4dpq8mm2",
|
||||||
"imageId": "cmkkx9e38000bvni4azjd3u53"
|
"imageName": "SQqSobKRg3ShvgPw_H41h-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkkrxmub0004vni41cwyhid6",
|
"id": "cmkkrxmub0004vni41cwyhid6",
|
||||||
@@ -33,6 +33,6 @@
|
|||||||
"deskripsiPengaduan": "<p>Pengelolaan Sampah Rumah Tangga Belum Efektif</p>",
|
"deskripsiPengaduan": "<p>Pengelolaan Sampah Rumah Tangga Belum Efektif</p>",
|
||||||
"lokasiKejadian": "Banjar Bucu",
|
"lokasiKejadian": "Banjar Bucu",
|
||||||
"jenisPengaduanId": "eommt91ma000004lb4dpq7ll1",
|
"jenisPengaduanId": "eommt91ma000004lb4dpq7ll1",
|
||||||
"imageId": "cmkky60sq0000vnjjc55k84d2"
|
"imageName": "y78xZ2axTOjz87gRKjVAf-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
"id": "cmkc2tcs00002vnt9c0ssj05n",
|
"id": "cmkc2tcs00002vnt9c0ssj05n",
|
||||||
"name": "Sosialisasi dan Pembinaan Keamanan Lingkungan Desa Darmasaba",
|
"name": "Sosialisasi dan Pembinaan Keamanan Lingkungan Desa Darmasaba",
|
||||||
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan Sosialisasi dan Pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai di Wantilan Perum Darmasaba Permai, Desa Darmasaba. Kegiatan ini melibatkan Perbekel Darmasaba, Bhabinkamtibmas, Babinsa, anggota BPD, LPM Desa, KBD dan KBA untuk mengajak warga berperan aktif dalam menjaga keamanan lingkungan, serta mendukung pemasangan lampu penerangan jalan guna mencegah kriminalitas dan kecelakaan di wilayah lingkungan.</p>",
|
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan Sosialisasi dan Pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai di Wantilan Perum Darmasaba Permai, Desa Darmasaba. Kegiatan ini melibatkan Perbekel Darmasaba, Bhabinkamtibmas, Babinsa, anggota BPD, LPM Desa, KBD dan KBA untuk mengajak warga berperan aktif dalam menjaga keamanan lingkungan, serta mendukung pemasangan lampu penerangan jalan guna mencegah kriminalitas dan kecelakaan di wilayah lingkungan.</p>",
|
||||||
"imageId": "cmkc2tcn30000vnt9esmx8kyb"
|
"imageName": "K0wY911212dinYA3AFB_f-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkc2xmdh0005vnt9ri6f4nk8",
|
"id": "cmkc2xmdh0005vnt9ri6f4nk8",
|
||||||
"name": "Sinergi Aparat dan Masyarakat untuk Keamanan Lingkungan",
|
"name": "Sinergi Aparat dan Masyarakat untuk Keamanan Lingkungan",
|
||||||
"deskripsi": "<p>Desa Darmasaba bersama aparat seperti Polres Badung dan elemen masyarakat berkomitmen menjalin sinergi untuk menciptakan keamanan dan ketertiban lingkungan yang kondusif, memperkuat kepedulian serta tindakan nyata dalam menjaga situasi kamtibmas desa.</p>",
|
"deskripsi": "<p>Desa Darmasaba bersama aparat seperti Polres Badung dan elemen masyarakat berkomitmen menjalin sinergi untuk menciptakan keamanan dan ketertiban lingkungan yang kondusif, memperkuat kepedulian serta tindakan nyata dalam menjaga situasi kamtibmas desa.</p>",
|
||||||
"imageId": "cmkc2xm1z0003vnt98682dv0a"
|
"imageName": "x0_-siY2V8IehBzo4_uph-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkc36qbl0008vnt9odvekex6",
|
"id": "cmkc36qbl0008vnt9odvekex6",
|
||||||
"name": "Peran Sistem Keamanan Lingkungan (Siskamling) dan Pecalang di Bali",
|
"name": "Peran Sistem Keamanan Lingkungan (Siskamling) dan Pecalang di Bali",
|
||||||
"deskripsi": "<p>Sistem keamanan lingkungan (Siskamling) di Bali termasuk di Desa Darmasaba melibatkan kolaborasi antara pemerintah desa, satlinmas, dan pecalang sebagai pranata adat Bali. Sinergi ini penting untuk menjaga ketertiban masyarakat serta harmoni sosial berdasarkan kearifan lokal seperti Tri Hita Karana, meskipun perlu pembinaan dan koordinasi terus menerus dari desa dan aparat terkait.</p>",
|
"deskripsi": "<p>Sistem keamanan lingkungan (Siskamling) di Bali termasuk di Desa Darmasaba melibatkan kolaborasi antara pemerintah desa, satlinmas, dan pecalang sebagai pranata adat Bali. Sinergi ini penting untuk menjaga ketertiban masyarakat serta harmoni sosial berdasarkan kearifan lokal seperti Tri Hita Karana, meskipun perlu pembinaan dan koordinasi terus menerus dari desa dan aparat terkait.</p>",
|
||||||
"imageId": "cmkc36q2j0006vnt9g87h5it4"
|
"imageName": "TXknK9CSRSxwvM2hPW6BO-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
"id": "cmkp70zau0002vnu9o1jtpi1i",
|
"id": "cmkp70zau0002vnu9o1jtpi1i",
|
||||||
"judul": "Keamanan Rumah",
|
"judul": "Keamanan Rumah",
|
||||||
"deskripsi": "<p><ul><li><p>Pastikan pintu dan jendela selalu terkunci saat meninggalkan rumah</p></li><li><p>Pasang lampu penerangan di halaman dan area sekitar rumah untuk mencegah tindak kejahatan.</p></li><li><p>Jangan mudah memberikan akses masuk ke orang yang tidak dikenal.</p></li></ul></p>",
|
"deskripsi": "<p><ul><li><p>Pastikan pintu dan jendela selalu terkunci saat meninggalkan rumah</p></li><li><p>Pasang lampu penerangan di halaman dan area sekitar rumah untuk mencegah tindak kejahatan.</p></li><li><p>Jangan mudah memberikan akses masuk ke orang yang tidak dikenal.</p></li></ul></p>",
|
||||||
"imageId": "cmkp71pub0003vnu9ef60huuv"
|
"imageName": "dSe0xyvNLkP2t2f6iq-Hk-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkp71pzo0005vnu9p3n9646d",
|
"id": "cmkp71pzo0005vnu9p3n9646d",
|
||||||
"judul": "Keamanan Lingkungan Tanggungjawab Bersama",
|
"judul": "Keamanan Lingkungan Tanggungjawab Bersama",
|
||||||
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan sosialisasi dan pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai. Warga diajak berperan aktif dalam menjaga keamanan lingkungan serta mendukung penyediaan lampu penerangan jalan untuk mencegah tindak kriminal dan kecelakaan. Bhabinkamtibmas dan Babinsa turut memberikan materi keamanan dan ketertiban kepada warga, menekankan pentingnya partisipasi masyarakat dalam menjaga keamanan desa.</p>",
|
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan sosialisasi dan pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai. Warga diajak berperan aktif dalam menjaga keamanan lingkungan serta mendukung penyediaan lampu penerangan jalan untuk mencegah tindak kriminal dan kecelakaan. Bhabinkamtibmas dan Babinsa turut memberikan materi keamanan dan ketertiban kepada warga, menekankan pentingnya partisipasi masyarakat dalam menjaga keamanan desa.</p>",
|
||||||
"imageId": "cmkp70z5g0000vnu9b0aieem8"
|
"imageName": "vwZsaxcoFWDlxG1PW7FC0-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -4,34 +4,34 @@
|
|||||||
"name": "Diare dan Kolera",
|
"name": "Diare dan Kolera",
|
||||||
"deskripsiSingkat": "<p>Apa itu Diare dan Kolera penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan Diare dan Kolera yang efektif untuk melindungi keluarga anda.</p>",
|
"deskripsiSingkat": "<p>Apa itu Diare dan Kolera penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan Diare dan Kolera yang efektif untuk melindungi keluarga anda.</p>",
|
||||||
"deskripsiLengkap": "<p>Apa itu Diare dan Kolera penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan Diare dan Kolera yang efektif untuk melindungi keluarga anda.</p><ul><li><p>Penyebab: Bakteri Vibrio cholerae (Kolera) atau Escherichia coli (diare) akibat makanan/minuman yang terkontaminasi.</p></li><li><p>Gejala: Buang air besar cair terus-menerus, dehidrasi, dan lemas. Pencegahan: Menjaga kebersihan makanan dan air, serta mencuci tangan dengan sabun.</p></li></ul>",
|
"deskripsiLengkap": "<p>Apa itu Diare dan Kolera penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan Diare dan Kolera yang efektif untuk melindungi keluarga anda.</p><ul><li><p>Penyebab: Bakteri Vibrio cholerae (Kolera) atau Escherichia coli (diare) akibat makanan/minuman yang terkontaminasi.</p></li><li><p>Gejala: Buang air besar cair terus-menerus, dehidrasi, dan lemas. Pencegahan: Menjaga kebersihan makanan dan air, serta mencuci tangan dengan sabun.</p></li></ul>",
|
||||||
"imageId": "cmkax3o8g000rvn6ygqpmo1nb"
|
"imageName": "5giLSHSnWEFoZoMEcjhL7-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkax5urc000wvn6yxfw0970w",
|
"id": "cmkax5urc000wvn6yxfw0970w",
|
||||||
"name": "TBC (Tuberkulosis)",
|
"name": "TBC (Tuberkulosis)",
|
||||||
"deskripsiSingkat": "<p>Apa itu TBC penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan TBC yang efektif untuk melindungi keluarga anda.</p>",
|
"deskripsiSingkat": "<p>Apa itu TBC penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan TBC yang efektif untuk melindungi keluarga anda.</p>",
|
||||||
"deskripsiLengkap": "<p>Apa itu TBC penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan TBC yang efektif untuk melindungi keluarga anda.</p><p>Penyebab: Bakteri Mycobacterium tuberculosis yang menyebar melalui udara.</p><p>Gejala: Batuk lebih dari 2 minggu, berkeringat di malam hari, dan berat badan turun.</p><p>Pencegahan: Vaksin BCG, pola hidup sehat, dan pengobatan bagi penderita agar tidak menular.</p>",
|
"deskripsiLengkap": "<p>Apa itu TBC penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan TBC yang efektif untuk melindungi keluarga anda.</p><p>Penyebab: Bakteri Mycobacterium tuberculosis yang menyebar melalui udara.</p><p>Gejala: Batuk lebih dari 2 minggu, berkeringat di malam hari, dan berat badan turun.</p><p>Pencegahan: Vaksin BCG, pola hidup sehat, dan pengobatan bagi penderita agar tidak menular.</p>",
|
||||||
"imageId": "cmkax5ukz000uvn6yho3aj2nf"
|
"imageName": "3faPo-1wjhVDVU6S7S8sS-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkax72s7000zvn6yz3nmvrry",
|
"id": "cmkax72s7000zvn6yz3nmvrry",
|
||||||
"name": "Demam Berdarah Dengue (DBD)",
|
"name": "Demam Berdarah Dengue (DBD)",
|
||||||
"deskripsiSingkat": "<p>Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.</p>",
|
"deskripsiSingkat": "<p>Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.</p>",
|
||||||
"deskripsiLengkap": "<p>Apa itu DBD penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.</p><p>Penyebab: Virus dengue yang ditularkan oleh nyamuk Aedes aegypti.</p><p>Gejala: Demam tinggi, nyeri sendi, ruam kulit, dan pendarahan ringan.</p><p>Pencegahan: Menguras tempat air, menutup wadah air, fogging, dan menggunakan lotion anti-nyamuk.</p>",
|
"deskripsiLengkap": "<p>Apa itu DBD penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.</p><p>Penyebab: Virus dengue yang ditularkan oleh nyamuk Aedes aegypti.</p><p>Gejala: Demam tinggi, nyeri sendi, ruam kulit, dan pendarahan ringan.</p><p>Pencegahan: Menguras tempat air, menutup wadah air, fogging, dan menggunakan lotion anti-nyamuk.</p>",
|
||||||
"imageId": "cmkax72nw000xvn6ymcuvlzom"
|
"imageName": "DyX82oztXbHfu6HEvbrpt-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkbyny4f0002vn67kmjmjrpl",
|
"id": "cmkbyny4f0002vn67kmjmjrpl",
|
||||||
"name": "Fogging sebagai Pencegah DBD di Br. Umahanyar Desa Darmasaba",
|
"name": "Fogging sebagai Pencegah DBD di Br. Umahanyar Desa Darmasaba",
|
||||||
"deskripsiSingkat": "<p>Pemerintah Desa Darmasaba melaksanakan fogging di wilayah Br. Umahanyar sebagai upaya pencegahan DBD di Desa Darmasaba.</p>",
|
"deskripsiSingkat": "<p>Pemerintah Desa Darmasaba melaksanakan fogging di wilayah Br. Umahanyar sebagai upaya pencegahan DBD di Desa Darmasaba.</p>",
|
||||||
"deskripsiLengkap": "<p>Pemerintah Desa Darmasaba melaksanakan fogging (pengasapan) di wilayah Br. Umahanyar Desa Darmasaba Kecamatan Abiansemal Kabupaten Badung dari tanggal 12 sampai dengan 13 April 2023.</p><p>Fogging ini merupakan salah satu metode yang dilakukan oleh Pemdes Darmasaba dalam pencegahan penyakit Demam Berdarah Dengue (DBD) dengan menargetkan nyamuk Aedes aegypti sebagai vektor penyebabnya.</p>",
|
"deskripsiLengkap": "<p>Pemerintah Desa Darmasaba melaksanakan fogging (pengasapan) di wilayah Br. Umahanyar Desa Darmasaba Kecamatan Abiansemal Kabupaten Badung dari tanggal 12 sampai dengan 13 April 2023.</p><p>Fogging ini merupakan salah satu metode yang dilakukan oleh Pemdes Darmasaba dalam pencegahan penyakit Demam Berdarah Dengue (DBD) dengan menargetkan nyamuk Aedes aegypti sebagai vektor penyebabnya.</p>",
|
||||||
"imageId": "cmkbynxxo0000vn67wi2nsyl3"
|
"imageName": "pps1ZgzJxDb4VZxEvtZeu-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkbyr3rx0005vn674uhycsxc",
|
"id": "cmkbyr3rx0005vn674uhycsxc",
|
||||||
"name": "Gerakan Serentak Penyemprotan Pencegahan PMK di Desa Darmasaba",
|
"name": "Gerakan Serentak Penyemprotan Pencegahan PMK di Desa Darmasaba",
|
||||||
"deskripsiSingkat": "<p>Penyemprotan serentak dilakukan di Desa Darmasaba untuk mencegah Penyakit Mulut dan Kaki (PMK) pada hewan ternak.</p>",
|
"deskripsiSingkat": "<p>Penyemprotan serentak dilakukan di Desa Darmasaba untuk mencegah Penyakit Mulut dan Kaki (PMK) pada hewan ternak.</p>",
|
||||||
"deskripsiLengkap": "<p>Setelah dilakukan vaksinasi Penyakit Mulut dan Kaki (PMK) pada hewan ternak yaitu sapi di wilayah Desa Darmasaba, Pemerintah Desa Darmasaba melaksanakan gerakan serentak penyemprotan pencegahan PMK pada hari Rabu (20/7/2022) di seputaran wilayah Desa Darmasaba.</p><p>Upaya ini dilakukan sebagai bentuk pencegahan terhadap penyebaran PMK dan menjaga kesehatan hewan ternak di desa.</p>",
|
"deskripsiLengkap": "<p>Setelah dilakukan vaksinasi Penyakit Mulut dan Kaki (PMK) pada hewan ternak yaitu sapi di wilayah Desa Darmasaba, Pemerintah Desa Darmasaba melaksanakan gerakan serentak penyemprotan pencegahan PMK pada hari Rabu (20/7/2022) di seputaran wilayah Desa Darmasaba.</p><p>Upaya ini dilakukan sebagai bentuk pencegahan terhadap penyebaran PMK dan menjaga kesehatan hewan ternak di desa.</p>",
|
||||||
"imageId": "cmkbyr3mk0003vn673xrqv8xv"
|
"imageName": "JhJigMo269K1TFGzSB1OS-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,28 +3,28 @@
|
|||||||
"id": "cmkax1vks000qvn6yyxuvfsi8",
|
"id": "cmkax1vks000qvn6yyxuvfsi8",
|
||||||
"name": "Puskesmas Pembantu Darmasaba",
|
"name": "Puskesmas Pembantu Darmasaba",
|
||||||
"deskripsi": "<p>Puskesmas Pembantu Darmasaba merupakan fasilitas kesehatan tingkat pertama yang berada di Desa Darmasaba, melayani berbagai layanan kesehatan masyarakat termasuk pemeriksaan umum dan imunisasi.</p>",
|
"deskripsi": "<p>Puskesmas Pembantu Darmasaba merupakan fasilitas kesehatan tingkat pertama yang berada di Desa Darmasaba, melayani berbagai layanan kesehatan masyarakat termasuk pemeriksaan umum dan imunisasi.</p>",
|
||||||
"imageId": "cmkb6488i001fvn6ylkddch1j",
|
"imageName": "g4ICsRrmOaIqS_yqlQLZK-mobile.webp",
|
||||||
"whatsapp": "089647037430"
|
"whatsapp": "089647037430"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawzrvg000nvn6ywyx529em",
|
"id": "cmkawzrvg000nvn6ywyx529em",
|
||||||
"name": "UPTD Puskesmas Abiansemal III (melayani Darmasaba)",
|
"name": "UPTD Puskesmas Abiansemal III (melayani Darmasaba)",
|
||||||
"deskripsi": "<p>Puskesmas Abiansemal III adalah fasilitas kesehatan utama di kecamatan Abiansemal yang melayani wilayah Desa Darmasaba dan sekitarnya. Puskesmas ini memiliki layanan 24 jam serta pelayanan darurat kesehatan dasar.</p>",
|
"deskripsi": "<p>Puskesmas Abiansemal III adalah fasilitas kesehatan utama di kecamatan Abiansemal yang melayani wilayah Desa Darmasaba dan sekitarnya. Puskesmas ini memiliki layanan 24 jam serta pelayanan darurat kesehatan dasar.</p>",
|
||||||
"imageId": "cmkb681og001gvn6ykb5uasln",
|
"imageName": "1NkzPzQailqE5yNOiUjB9-mobile.webp",
|
||||||
"whatsapp": "03618463263"
|
"whatsapp": "03618463263"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawy5in000kvn6yza82pkkg",
|
"id": "cmkawy5in000kvn6yza82pkkg",
|
||||||
"name": "UPTD Puskesmas Abiansemal I",
|
"name": "UPTD Puskesmas Abiansemal I",
|
||||||
"deskripsi": "<p>Puskesmas Abiansemal I melayani masyarakat di wilayah kecamatan Abiansemal, termasuk pelayanan kesehatan darurat dan program kesehatan masyarakat.</p>",
|
"deskripsi": "<p>Puskesmas Abiansemal I melayani masyarakat di wilayah kecamatan Abiansemal, termasuk pelayanan kesehatan darurat dan program kesehatan masyarakat.</p>",
|
||||||
"imageId": "cmkb6brrf0000vn14u8c7wnox",
|
"imageName": "NBPAqjPXn7GQmYTDBI5hu-mobile.webp",
|
||||||
"whatsapp": "087858367111"
|
"whatsapp": "087858367111"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkb6ehu20003vn14ca4xr057",
|
"id": "cmkb6ehu20003vn14ca4xr057",
|
||||||
"name": "Kantor Desa Darmasaba (Kontak Informasi Kesehatan)",
|
"name": "Kantor Desa Darmasaba (Kontak Informasi Kesehatan)",
|
||||||
"deskripsi": "<p>Kantor Pemerintahan Desa Darmasaba dapat menjadi saluran kontak awal untuk rujukan layanan kesehatan darurat atau informasi lebih lanjut mengenai fasilitas kesehatan di wilayah desa.</p>",
|
"deskripsi": "<p>Kantor Pemerintahan Desa Darmasaba dapat menjadi saluran kontak awal untuk rujukan layanan kesehatan darurat atau informasi lebih lanjut mengenai fasilitas kesehatan di wilayah desa.</p>",
|
||||||
"imageId": "cmkb6ehpi0001vn14hjp4tdye",
|
"imageName": "EcQIGOF6LW1dIKE53vmba-mobile.webp",
|
||||||
"whatsapp": "081239580000"
|
"whatsapp": "081239580000"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,24 +3,24 @@
|
|||||||
"id": "cmkawso7y000evn6ygob15cqb",
|
"id": "cmkawso7y000evn6ygob15cqb",
|
||||||
"name": "Rembug Stunting di Desa Darmasaba",
|
"name": "Rembug Stunting di Desa Darmasaba",
|
||||||
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan kegiatan rembug stunting dengan melibatkan bidan desa, kader posyandu, dan tokoh masyarakat. Tujuan kegiatan ini adalah untuk memperkuat upaya pencegahan kekerdilan (stunting) melalui koordinasi layanan kesehatan, edukasi gizi, serta percepatan penanganan gizi buruk di lingkungan desa sebagai bagian dari respons terhadap kondisi kesehatan yang mendesak.</p>",
|
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan kegiatan rembug stunting dengan melibatkan bidan desa, kader posyandu, dan tokoh masyarakat. Tujuan kegiatan ini adalah untuk memperkuat upaya pencegahan kekerdilan (stunting) melalui koordinasi layanan kesehatan, edukasi gizi, serta percepatan penanganan gizi buruk di lingkungan desa sebagai bagian dari respons terhadap kondisi kesehatan yang mendesak.</p>",
|
||||||
"imageId": "cmkayz2h8001cvn6yrb7uptjs"
|
"imageName": "Gi8EX3pBmT719AfzXirDS-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawq3ef000bvn6y387vub0y",
|
"id": "cmkawq3ef000bvn6y387vub0y",
|
||||||
"name": "Posko Kesehatan Darurat dan Bencana",
|
"name": "Posko Kesehatan Darurat dan Bencana",
|
||||||
"deskripsi": "<p>Posko Kesehatan Darurat dan Bencana Desa Darmasaba dibentuk sebagai pusat koordinasi dan pertolongan bagi warga yang terdampak situasi darurat seperti banjir, tanah longsor, atau wabah penyakit. Posko ini dilengkapi dengan tenaga medis, obat-obatan dasar, serta dukungan logistik untuk memastikan penanganan cepat dan tepat sasaran. Kegiatan ini juga melibatkan kader kesehatan desa dan karang taruna sebagai relawan lapangan.</p>",
|
"deskripsi": "<p>Posko Kesehatan Darurat dan Bencana Desa Darmasaba dibentuk sebagai pusat koordinasi dan pertolongan bagi warga yang terdampak situasi darurat seperti banjir, tanah longsor, atau wabah penyakit. Posko ini dilengkapi dengan tenaga medis, obat-obatan dasar, serta dukungan logistik untuk memastikan penanganan cepat dan tepat sasaran. Kegiatan ini juga melibatkan kader kesehatan desa dan karang taruna sebagai relawan lapangan.</p>",
|
||||||
"imageId": "cmkawq38m0009vn6yi7evbhap"
|
"imageName": "v7Ac2xQvTiJy-HYh1AxF4-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawso7y000evn6ygob14bpa",
|
"id": "cmkawso7y000evn6ygob14bpa",
|
||||||
"name": "Layanan Ambulans Desa Darmasaba",
|
"name": "Layanan Ambulans Desa Darmasaba",
|
||||||
"deskripsi": "<p>Layanan Ambulans Desa Darmasaba disiapkan untuk membantu masyarakat yang membutuhkan transportasi medis darurat ke fasilitas kesehatan terdekat. Layanan ini beroperasi 24 jam dan dapat dihubungi melalui nomor darurat desa. Tim ambulans terdiri dari relawan terlatih dan tenaga medis yang siap memberikan pertolongan pertama di lokasi kejadian sebelum dirujuk ke rumah sakit atau puskesmas.</p>",
|
"deskripsi": "<p>Layanan Ambulans Desa Darmasaba disiapkan untuk membantu masyarakat yang membutuhkan transportasi medis darurat ke fasilitas kesehatan terdekat. Layanan ini beroperasi 24 jam dan dapat dihubungi melalui nomor darurat desa. Tim ambulans terdiri dari relawan terlatih dan tenaga medis yang siap memberikan pertolongan pertama di lokasi kejadian sebelum dirujuk ke rumah sakit atau puskesmas.</p>",
|
||||||
"imageId": "cmkawso29000cvn6y879ahra0"
|
"imageName": "jYxEXspWH5g6eTTVqK72c-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawu7te000hvn6yh3pdnv4w",
|
"id": "cmkawu7te000hvn6yh3pdnv4w",
|
||||||
"name": "Penanganan Darurat Sosial & Kesehatan Desa Darmasaba",
|
"name": "Penanganan Darurat Sosial & Kesehatan Desa Darmasaba",
|
||||||
"deskripsi": "<p>Program Penanganan Darurat Sosial & Kesehatan Desa Darmasaba bertujuan memberikan respon cepat terhadap situasi darurat seperti warga sakit mendadak, kecelakaan, bencana alam, maupun kondisi sosial yang membutuhkan bantuan segera. Tim Siaga Desa Darmasaba berkoordinasi dengan Puskesmas Abiansemal dan BPBD untuk memastikan penanganan yang cepat, tepat, dan manusiawi. Program ini juga mencakup layanan ambulans desa, posko kesehatan darurat, serta bantuan logistik bagi warga terdampak.</p>",
|
"deskripsi": "<p>Program Penanganan Darurat Sosial & Kesehatan Desa Darmasaba bertujuan memberikan respon cepat terhadap situasi darurat seperti warga sakit mendadak, kecelakaan, bencana alam, maupun kondisi sosial yang membutuhkan bantuan segera. Tim Siaga Desa Darmasaba berkoordinasi dengan Puskesmas Abiansemal dan BPBD untuk memastikan penanganan yang cepat, tepat, dan manusiawi. Program ini juga mencakup layanan ambulans desa, posko kesehatan darurat, serta bantuan logistik bagi warga terdampak.</p>",
|
||||||
"imageId": "cmkawu7qj000fvn6yubhimyiv"
|
"imageName": "3tNQ9J8I3Ewq5H8CWuqvp-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
"nomor": "(0361) 8463263",
|
"nomor": "(0361) 8463263",
|
||||||
"deskripsi": "<p>Posyandu Pudak Amara merupakan salah satu posyandu aktif di Desa Darmasaba dan pernah berkompetisi dalam lomba kader dan posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p><p>Kegiatan ini melibatkan kader posyandu serta didampingi pihak desa dan puskesmas setempat untuk meningkatkan pelayanan kesehatan ibu dan anak.</p>",
|
"deskripsi": "<p>Posyandu Pudak Amara merupakan salah satu posyandu aktif di Desa Darmasaba dan pernah berkompetisi dalam lomba kader dan posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p><p>Kegiatan ini melibatkan kader posyandu serta didampingi pihak desa dan puskesmas setempat untuk meningkatkan pelayanan kesehatan ibu dan anak.</p>",
|
||||||
"jadwalPelayanan": "<p>Setiap bulan pada satu hari tertentu (mis. minggu ke-2): 08:00 – 12:00 WITA (posyandu balita & ibu hamil)</p>",
|
"jadwalPelayanan": "<p>Setiap bulan pada satu hari tertentu (mis. minggu ke-2): 08:00 – 12:00 WITA (posyandu balita & ibu hamil)</p>",
|
||||||
"imageId": "cmkanjnfh0004vntz8cdbxa7f"
|
"imageName": "TDQReg1lQ73s39crXW0ra-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,48 +4,48 @@
|
|||||||
"name": "Gerakan Kulkul PKK dan Posyandu Desa Darmasaba",
|
"name": "Gerakan Kulkul PKK dan Posyandu Desa Darmasaba",
|
||||||
"deskripsiSingkat": "<p>Kegiatan bersama PKK dan Posyandu untuk meningkatkan pelayanan kesehatan masyarakat.</p>",
|
"deskripsiSingkat": "<p>Kegiatan bersama PKK dan Posyandu untuk meningkatkan pelayanan kesehatan masyarakat.</p>",
|
||||||
"deskripsi": "<p>Pada hari Minggu, 11 Januari 2025, Pemerintah Desa Darmasaba melalui TP PKK dan TP Posyandu melaksanakan kegiatan Gerakan Kulkul PKK dan Posyandu yang berlangsung serentak di seluruh wilayah Desa Darmasaba untuk memperkuat pelayanan kesehatan dasar dan peningkatan partisipasi masyarakat dalam program Posyandu.</p>",
|
"deskripsi": "<p>Pada hari Minggu, 11 Januari 2025, Pemerintah Desa Darmasaba melalui TP PKK dan TP Posyandu melaksanakan kegiatan Gerakan Kulkul PKK dan Posyandu yang berlangsung serentak di seluruh wilayah Desa Darmasaba untuk memperkuat pelayanan kesehatan dasar dan peningkatan partisipasi masyarakat dalam program Posyandu.</p>",
|
||||||
"imageId": "cmkay1e590010vn6y24pgaa1r"
|
"imageName": "hLeF0GRFZqDUngZnDMAAk-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawmlg40005vn6yja2xiev0",
|
"id": "cmkawmlg40005vn6yja2xiev0",
|
||||||
"name": "Pendampingan Kunjungan Rumah oleh Puskesmas Abiansemal 3",
|
"name": "Pendampingan Kunjungan Rumah oleh Puskesmas Abiansemal 3",
|
||||||
"deskripsiSingkat": "<p>Pendataan kesehatan penyandang disabilitas lewat kunjungan rumah di Desa Darmasaba.</p>",
|
"deskripsiSingkat": "<p>Pendataan kesehatan penyandang disabilitas lewat kunjungan rumah di Desa Darmasaba.</p>",
|
||||||
"deskripsi": "<p>Pemerintah Desa Darmasaba bersama Kelian Banjar Dinas dan kader kesehatan mendampingi kegiatan kunjungan rumah yang dilaksanakan oleh Puskesmas Abiansemal 3 pada 21 Juli 2025, difokuskan pada pendataan dan pemantauan kondisi kesehatan penyandang disabilitas di Banjar Bersih, Desa Darmasaba.</p>",
|
"deskripsi": "<p>Pemerintah Desa Darmasaba bersama Kelian Banjar Dinas dan kader kesehatan mendampingi kegiatan kunjungan rumah yang dilaksanakan oleh Puskesmas Abiansemal 3 pada 21 Juli 2025, difokuskan pada pendataan dan pemantauan kondisi kesehatan penyandang disabilitas di Banjar Bersih, Desa Darmasaba.</p>",
|
||||||
"imageId": "cmkay6hob0011vn6ybjwejcej"
|
"imageName": "hyyTFi8EApjzFEZ9EvJgB-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawnr9k0008vn6ymwv0foiv",
|
"id": "cmkawnr9k0008vn6ymwv0foiv",
|
||||||
"name": "Kegiatan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali di Desa Darmasaba",
|
"name": "Kegiatan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali di Desa Darmasaba",
|
||||||
"deskripsiSingkat": "<p>Aksi sosial TP Posyandu Bali untuk memperkuat pelayanan posyandu di desa.</p>",
|
"deskripsiSingkat": "<p>Aksi sosial TP Posyandu Bali untuk memperkuat pelayanan posyandu di desa.</p>",
|
||||||
"deskripsi": "<p>Pada 10 Desember 2025, Desa Darmasaba menjadi lokasi pelaksanaan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali yang bertujuan memperkuat pelayanan Posyandu serta meningkatkan kesejahteraan masyarakat, khususnya keluarga dan balita.</p>",
|
"deskripsi": "<p>Pada 10 Desember 2025, Desa Darmasaba menjadi lokasi pelaksanaan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali yang bertujuan memperkuat pelayanan Posyandu serta meningkatkan kesejahteraan masyarakat, khususnya keluarga dan balita.</p>",
|
||||||
"imageId": "cmkay8vmd0012vn6ylsk2vzfo"
|
"imageName": "l4qsUEw2JiclGAkkrXp9g-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawnr9k0008vn6ymwv0dpjw",
|
"id": "cmkawnr9k0008vn6ymwv0dpjw",
|
||||||
"name": "Inovasi BAJRA dalam Penanggulangan Rabies",
|
"name": "Inovasi BAJRA dalam Penanggulangan Rabies",
|
||||||
"deskripsiSingkat": "<p>Program BAJRA untuk penanggulangan rabies di Desa Darmasaba.</p>",
|
"deskripsiSingkat": "<p>Program BAJRA untuk penanggulangan rabies di Desa Darmasaba.</p>",
|
||||||
"deskripsi": "<p>Desa Darmasaba mengembangkan inovasi BAJRA (Bersama Jaga Rabies), sebuah program berbasis komunitas untuk penanggulangan rabies yang mengintegrasikan pelaporan cepat masyarakat, edukasi berkelanjutan dan koordinasi lintas sektor antara kesehatan hewan, manusia, dan pemerintahan desa.</p>",
|
"deskripsi": "<p>Desa Darmasaba mengembangkan inovasi BAJRA (Bersama Jaga Rabies), sebuah program berbasis komunitas untuk penanggulangan rabies yang mengintegrasikan pelaporan cepat masyarakat, edukasi berkelanjutan dan koordinasi lintas sektor antara kesehatan hewan, manusia, dan pemerintahan desa.</p>",
|
||||||
"imageId": "cmkayd8o90013vn6ye7n8805q"
|
"imageName": "Gc79mlIlGuoRQuTqskFj--mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawnr9k0008vn6ymwv0eqkx",
|
"id": "cmkawnr9k0008vn6ymwv0eqkx",
|
||||||
"name": "Posyandu Pudak Amara Berkompetisi",
|
"name": "Posyandu Pudak Amara Berkompetisi",
|
||||||
"deskripsiSingkat": "<p>Partisipasi Posyandu Pudak Amara dalam lomba prestasi Posyandu tingkat provinsi.</p>",
|
"deskripsiSingkat": "<p>Partisipasi Posyandu Pudak Amara dalam lomba prestasi Posyandu tingkat provinsi.</p>",
|
||||||
"deskripsi": "<p>Kader Posyandu Pudak Amara Br. Cabe mendapat pendampingan dari Perbekel Darmasaba, Dinas Kesehatan Kab. Badung, Puskesmas Abiansemal III, dan Pustu Desa Darmasaba dalam ajang lomba kader dan Posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p>",
|
"deskripsi": "<p>Kader Posyandu Pudak Amara Br. Cabe mendapat pendampingan dari Perbekel Darmasaba, Dinas Kesehatan Kab. Badung, Puskesmas Abiansemal III, dan Pustu Desa Darmasaba dalam ajang lomba kader dan Posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p>",
|
||||||
"imageId": "cmkayi0x90016vn6ykddxqyq3"
|
"imageName": "OsMY3AYPyGC_CoN1xUjOn-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkawnr9k0008vn6ymwv1frly",
|
"id": "cmkawnr9k0008vn6ymwv1frly",
|
||||||
"name": "Outbound Kader Posyandu Darmasaba",
|
"name": "Outbound Kader Posyandu Darmasaba",
|
||||||
"deskripsiSingkat": "<p>Program pembinaan dan pengembangan kapasitas kader Posyandu.</p>",
|
"deskripsiSingkat": "<p>Program pembinaan dan pengembangan kapasitas kader Posyandu.</p>",
|
||||||
"deskripsi": "<p>Pemdes Darmasaba melaksanakan kegiatan Outbound Posyandu untuk meningkatkan kapasitas dan wawasan Kader Posyandu se-Desa Darmasaba sebagai bagian dari upaya peningkatan kualitas pelayanan kesehatan dasar di masyarakat.</p>",
|
"deskripsi": "<p>Pemdes Darmasaba melaksanakan kegiatan Outbound Posyandu untuk meningkatkan kapasitas dan wawasan Kader Posyandu se-Desa Darmasaba sebagai bagian dari upaya peningkatan kualitas pelayanan kesehatan dasar di masyarakat.</p>",
|
||||||
"imageId": "cmkaykipf0019vn6yknjno3k1"
|
"imageName": "M9QlgVKIEfCdY3g4F_tRZ-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkdu8ki10004vn4lpbxm2zqo",
|
"id": "cmkdu8ki10004vn4lpbxm2zqo",
|
||||||
"name": "PEMBANGUNAN JAMBAN BAGI MASYARAKAT",
|
"name": "PEMBANGUNAN JAMBAN BAGI MASYARAKAT",
|
||||||
"deskripsiSingkat": "<p>Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p>",
|
"deskripsiSingkat": "<p>Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p>",
|
||||||
"deskripsi": "<p>Desa Darmasaba sebagai desa yang berkomitmen selalu selaras dengan pembangunan Pemerintah Kabupaten Badung pada tahun anggaran 2023 ini turut ambil bagian dalam menyukseskan program Bupati Badung I Nyoman Giri Prasta, S.Sos dalam bidang kesehatan sanitasi masyarakat. Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p><p style=\"text-align: justify\">Pemberian bantuan jamban ini dilaksanakan di 11 banjar dengan menyasar 22 keluarga yang memang belum memiliki jamban yang sumber dananya sepenuhnya dari APBDes Darmasaba T. A. 2023. Pembangunan Jamban bagi Masyarakat ini juga menjadi bukti komitmen Pemerintah Desa Darmasaba dalam melaksanakan salah satu visi mewujudkan masyarakat yang sejahtera dan berbudaya untuk menjaga lingkungan yang bersih dan sehat.</p>",
|
"deskripsi": "<p>Desa Darmasaba sebagai desa yang berkomitmen selalu selaras dengan pembangunan Pemerintah Kabupaten Badung pada tahun anggaran 2023 ini turut ambil bagian dalam menyukseskan program Bupati Badung I Nyoman Giri Prasta, S.Sos dalam bidang kesehatan sanitasi masyarakat. Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p><p style=\"text-align: justify\">Pemberian bantuan jamban ini dilaksanakan di 11 banjar dengan menyasar 22 keluarga yang memang belum memiliki jamban yang sumber dananya sepenuhnya dari APBDes Darmasaba T. A. 2023. Pembangunan Jamban bagi Masyarakat ini juga menjadi bukti komitmen Pemerintah Desa Darmasaba dalam melaksanakan salah satu visi mewujudkan masyarakat yang sejahtera dan berbudaya untuk menjaga lingkungan yang bersih dan sehat.</p>",
|
||||||
"imageId": "cmkdu8kb20002vn4lihwo4k86"
|
"imageName": "6DQbAvn0St-xHdPGW3vpY-mobile.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"name": "Puskesmas Abiansemal III",
|
"name": "Puskesmas Abiansemal III",
|
||||||
"alamat": "Jl. Ratna, Sibang Kaja, Abiansemal, Badung, Bali 80352",
|
"alamat": "Jl. Ratna, Sibang Kaja, Abiansemal, Badung, Bali 80352",
|
||||||
"jamId": "cmkao2zwx0008vntzmvqdsdzo",
|
"jamId": "cmkao2zwx0008vntzmvqdsdzo",
|
||||||
"imageId": "cmkao2zm90007vntzxqkjy5mt",
|
"imageName": "d6hJgycQawWN3VEcHaqtR-mobile.webp",
|
||||||
"kontakId": "cmkao2zxc0009vntz00kev051"
|
"kontakId": "cmkao2zxc0009vntz00kev051"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"name": "Puskesmas Pembantu Darmasaba",
|
"name": "Puskesmas Pembantu Darmasaba",
|
||||||
"alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali",
|
"alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali",
|
||||||
"jamId": "cmkao2zwx0008vntzmvqdseal",
|
"jamId": "cmkao2zwx0008vntzmvqdseal",
|
||||||
"imageId": "cmkatoru10000vny38y0wxd6s",
|
"imageName": "cg78Sb_QzZFlli9s2FPVc-mobile.webp",
|
||||||
"kontakId": "cmkao2zxc0009vntz00kev162"
|
"kontakId": "cmkao2zxc0009vntz00kev162"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"tanggal": "2024-01-28T00:00:00.000Z",
|
"tanggal": "2024-01-28T00:00:00.000Z",
|
||||||
"lokasi": "Pura Desa dan Pura Dalem, Desa Adat Tegal, Desa Darmasaba, Badung",
|
"lokasi": "Pura Desa dan Pura Dalem, Desa Adat Tegal, Desa Darmasaba, Badung",
|
||||||
"partisipan": 30,
|
"partisipan": 30,
|
||||||
"imageId": "cmknb59md0000vnmam828iuzt",
|
"imageName": "YgOX5qAP3O1PHG5XmQXkr-mobile.webp",
|
||||||
"kategoriKegiatanId": "cmknan39v000004l8eiql149r"
|
"kategoriKegiatanId": "cmknan39v000004l8eiql149r"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"tanggal": "2023-11-17T00:00:00.000Z",
|
"tanggal": "2023-11-17T00:00:00.000Z",
|
||||||
"lokasi": "Desa Darmasaba, Badung",
|
"lokasi": "Desa Darmasaba, Badung",
|
||||||
"partisipan": 25,
|
"partisipan": 25,
|
||||||
"imageId": "cmknbp3vd0001vnmarjz542o7",
|
"imageName": "qxqSDHe-akIRi1EkQFUbG-mobile.webp",
|
||||||
"kategoriKegiatanId": "cmknan39v000004l8eiql149r"
|
"kategoriKegiatanId": "cmknan39v000004l8eiql149r"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"tanggal": "2022-05-26T00:00:00.000Z",
|
"tanggal": "2022-05-26T00:00:00.000Z",
|
||||||
"lokasi": "Pura Dalem Kangin, Desa Adat Tegal, Desa Darmasaba, Badung",
|
"lokasi": "Pura Dalem Kangin, Desa Adat Tegal, Desa Darmasaba, Badung",
|
||||||
"partisipan": 28,
|
"partisipan": 28,
|
||||||
"imageId": "cmknbrj4r0002vnmantw9rn0l",
|
"imageName": "iHTVkQZ1VdkMOXLt5qdAd-mobile.webp",
|
||||||
"kategoriKegiatanId": "cmknan39v000004l8eiql149r"
|
"kategoriKegiatanId": "cmknan39v000004l8eiql149r"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,139 +4,195 @@
|
|||||||
"judul": "Laskar Pelangi",
|
"judul": "Laskar Pelangi",
|
||||||
"deskripsi": "<p>Novel inspiratif tentang perjuangan anak-anak di Belitung dalam meraih pendidikan dan mimpi mereka</p>",
|
"deskripsi": "<p>Novel inspiratif tentang perjuangan anak-anak di Belitung dalam meraih pendidikan dan mimpi mereka</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibq76bdzu",
|
"kategoriId": "cmkqb11mc000104jibq76bdzu",
|
||||||
"imageId": "cmkqhbhxi0000vneamj3din9u"
|
"imageName": "RnAdv7O0QAFrxkFLAXJSa-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqhedff0005vneas3rtbumi",
|
"id": "cmkqhedff0005vneas3rtbumi",
|
||||||
"judul": "Bumi Manusia",
|
"judul": "Bumi Manusia",
|
||||||
"deskripsi": "<p>Kisah kehidupan Minke di masa kolonial yang menggambarkan perjuangan, pendidikan, dan identitas bangsa</p>",
|
"deskripsi": "<p>Kisah kehidupan Minke di masa kolonial yang menggambarkan perjuangan, pendidikan, dan identitas bangsa</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqc7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqc7bdzu",
|
||||||
"imageId": "cmkqhed8x0003vneakx0c7me2"
|
"imageName": "71eZShq4FYAFLxpLfZB0W-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqhg1g70008vneajbpz8phh",
|
"id": "cmkqhg1g70008vneajbpz8phh",
|
||||||
"judul": "Atomic Habits",
|
"judul": "Atomic Habits",
|
||||||
"deskripsi": "<p>Panduan membangun kebiasaan kecil yang konsisten untuk menghasilkan perubahan besar dalam hidup</p>",
|
"deskripsi": "<p>Panduan membangun kebiasaan kecil yang konsisten untuk menghasilkan perubahan besar dalam hidup</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqf7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqf7bdzu",
|
||||||
"imageId": "cmkqhg1cb0006vneagsxa6t4t"
|
"imageName": "Uxq3GXPqh7HN9fHmRkr3r-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqhl6sr000bvneampx0svus",
|
"id": "cmkqhl6sr000bvneampx0svus",
|
||||||
"judul": "Clean Code",
|
"judul": "Clean Code",
|
||||||
"deskripsi": "<p>Buku wajib programmer tentang cara menulis kode yang bersih, mudah dibaca, dan mudah dirawat</p>",
|
"deskripsi": "<p>Buku wajib programmer tentang cara menulis kode yang bersih, mudah dibaca, dan mudah dirawat</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqd7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqd7bdzu",
|
||||||
"imageId": "cmkqhl6mv0009vneasgix42ud"
|
"imageName": "W5Fc0uRADNkIY3nZicvQA-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqhoaa1000evnearppgpyxo",
|
"id": "cmkqhoaa1000evnearppgpyxo",
|
||||||
"judul": "Sejarah Indonesia Modern",
|
"judul": "Sejarah Indonesia Modern",
|
||||||
"deskripsi": "<p>Membahas perjalanan sejarah Indonesia dari masa kolonial hingga era modern</p>",
|
"deskripsi": "<p>Membahas perjalanan sejarah Indonesia dari masa kolonial hingga era modern</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqc7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqc7bdzu",
|
||||||
"imageId": "cmkqhoa5w000cvneah15n28zq"
|
"imageName": "mp77Op-MwtPQZnH3so4JY-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqhr9oc000hvnea677ad3kb",
|
"id": "cmkqhr9oc000hvnea677ad3kb",
|
||||||
"judul": "Ensiklopedia Anak Pintar",
|
"judul": "Ensiklopedia Anak Pintar",
|
||||||
"deskripsi": "<p>Buku referensi bergambar yang membantu anak mengenal ilmu pengetahuan secara menyenangkan</p>",
|
"deskripsi": "<p>Buku referensi bergambar yang membantu anak mengenal ilmu pengetahuan secara menyenangkan</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqh7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqh7bdzu",
|
||||||
"imageId": "cmkqhr9lg000fvneai3q8qw0s"
|
"imageName": "V09ZxN1wOwbSFLQiDK0VQ-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqi5ksf000kvnea9c04n2hy",
|
"id": "cmkqi5ksf000kvnea9c04n2hy",
|
||||||
"judul": "Filosofi Teras",
|
"judul": "Filosofi Teras",
|
||||||
"deskripsi": "<p>Pengenalan filsafat Stoikisme untuk menghadapi kehidupan modern dengan lebih tenang</p>",
|
"deskripsi": "<p>Pengenalan filsafat Stoikisme untuk menghadapi kehidupan modern dengan lebih tenang</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
||||||
"imageId": "cmkqi5knc000ivnea8grp7j06"
|
"imageName": "Wqp4AyVkGjqRMED9Q5XAs-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqi97hq000nvneaparjbcrm",
|
"id": "cmkqi97hq000nvneaparjbcrm",
|
||||||
"judul": "Pemrograman JavaScript Dasar",
|
"judul": "Pemrograman JavaScript Dasar",
|
||||||
"deskripsi": "<p>Panduan dasar belajar JavaScript untuk pemula dalam dunia pengembangan web</p>",
|
"deskripsi": "<p>Panduan dasar belajar JavaScript untuk pemula dalam dunia pengembangan web</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqd7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqd7bdzu",
|
||||||
"imageId": "cmkqi9799000lvneamskmvpq5"
|
"imageName": "NH4aLc7cVuutdQBCofTC0-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqibjt9000qvnea13ox7fmv",
|
"id": "cmkqibjt9000qvnea13ox7fmv",
|
||||||
"judul": "Pendidikan Karakter",
|
"judul": "Pendidikan Karakter",
|
||||||
"deskripsi": "<p>Buku yang membahas pentingnya pendidikan karakter dalam membentuk generasi bangsa</p>",
|
"deskripsi": "<p>Buku yang membahas pentingnya pendidikan karakter dalam membentuk generasi bangsa</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqf7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqf7bdzu",
|
||||||
"imageId": "cmkqibjj2000ovnea3zmmvdop"
|
"imageName": "MLrsPrD6oiHsrNP4Lc8J7-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqidnar000tvneaohk5v8k6",
|
"id": "cmkqidnar000tvneaohk5v8k6",
|
||||||
"judul": "Psikologi Kepribadian",
|
"judul": "Psikologi Kepribadian",
|
||||||
"deskripsi": "<p>Mengenal teori-teori kepribadian manusia dalam perspektif psikologi</p>",
|
"deskripsi": "<p>Mengenal teori-teori kepribadian manusia dalam perspektif psikologi</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
||||||
"imageId": "cmkqidn7e000rvnea5rl58f2e"
|
"imageName": "iaIeNdhuxqltqKP7aZncQ-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqifdiu000wvnea7xd0yi4f",
|
"id": "cmkqifdiu000wvnea7xd0yi4f",
|
||||||
"judul": "Ayat-Ayat Cinta",
|
"judul": "Ayat-Ayat Cinta",
|
||||||
"deskripsi": "<p>Novel religi yang mengangkat kisah cinta, iman, dan perjuangan hidup</p>",
|
"deskripsi": "<p>Novel religi yang mengangkat kisah cinta, iman, dan perjuangan hidup</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqe7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqe7bdzu",
|
||||||
"imageId": "cmkqifdfs000uvneajss8zswp"
|
"imageName": "WUDssJ59pTKE_3IuTiZ2s-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqik7vi000zvneae7d5cq9i",
|
"id": "cmkqik7vi000zvneae7d5cq9i",
|
||||||
"judul": "Negeri 5 Menara",
|
"judul": "Negeri 5 Menara",
|
||||||
"deskripsi": "<p>Cerita persahabatan dan perjuangan santri dalam mengejar mimpi hingga ke mancanegara</p>",
|
"deskripsi": "<p>Cerita persahabatan dan perjuangan santri dalam mengejar mimpi hingga ke mancanegara</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibq76bdzu",
|
"kategoriId": "cmkqb11mc000104jibq76bdzu",
|
||||||
"imageId": "cmkqik7p5000xvnea6krii3vw"
|
"imageName": "RJH_-4_R_nlP7GVEQeD1M-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqinno30012vneac1sgsvis",
|
"id": "cmkqinno30012vneac1sgsvis",
|
||||||
"judul": "Belajar UI/UX Design",
|
"judul": "Belajar UI/UX Design",
|
||||||
"deskripsi": "<p>Panduan praktis memahami desain antarmuka dan pengalaman pengguna</p>",
|
"deskripsi": "<p>Panduan praktis memahami desain antarmuka dan pengalaman pengguna</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqd7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqd7bdzu",
|
||||||
"imageId": "cmkqinnih0010vneakpjb9egl"
|
"imageName": "9MA-Jx_36uoho2Tg40_G9-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqiqegd0015vneawv5u5tpm",
|
"id": "cmkqiqegd0015vneawv5u5tpm",
|
||||||
"judul": "Manajemen Waktu Efektif",
|
"judul": "Manajemen Waktu Efektif",
|
||||||
"deskripsi": "<p>Teknik mengatur waktu agar lebih produktif dan fokus pada hal penting</p>",
|
"deskripsi": "<p>Teknik mengatur waktu agar lebih produktif dan fokus pada hal penting</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqf7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqf7bdzu",
|
||||||
"imageId": "cmkqiqeb60013vnea2ygrq5rs"
|
"imageName": "dkb7ZWFl28TREVcvH8sWd-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqiurc60018vneavyd3pj9q",
|
"id": "cmkqiurc60018vneavyd3pj9q",
|
||||||
"judul": "Dongeng Nusantara",
|
"judul": "Dongeng Nusantara",
|
||||||
"deskripsi": "<p>Kumpulan dongeng tradisional Indonesia yang sarat pesan moral</p>",
|
"deskripsi": "<p>Kumpulan dongeng tradisional Indonesia yang sarat pesan moral</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibq76bdzu",
|
"kategoriId": "cmkqb11mc000104jibq76bdzu",
|
||||||
"imageId": "cmkqiur960016vnea3werdoey"
|
"imageName": "nVj3one6CLuWRd04QnsWo-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqix2kb001bvnea5v81cw7p",
|
"id": "cmkqix2kb001bvnea5v81cw7p",
|
||||||
"judul": "Ekonomi Makro",
|
"judul": "Ekonomi Makro",
|
||||||
"deskripsi": "<p>Pembahasan konsep ekonomi makro secara sistematis dan mudah dipahami</p>",
|
"deskripsi": "<p>Pembahasan konsep ekonomi makro secara sistematis dan mudah dipahami</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
||||||
"imageId": "cmkqix2go0019vnea8coousvn"
|
"imageName": "AnB7JO4_6tlPTX3ypOVLi-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqiyts2001evneahnk45ry5",
|
"id": "cmkqiyts2001evneahnk45ry5",
|
||||||
"judul": "Seni Berpikir Kritis",
|
"judul": "Seni Berpikir Kritis",
|
||||||
"deskripsi": "<p>Buku yang membantu pembaca menghindari kesalahan berpikir dalam pengambilan keputusan</p>",
|
"deskripsi": "<p>Buku yang membantu pembaca menghindari kesalahan berpikir dalam pengambilan keputusan</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
||||||
"imageId": "cmkqiytnv001cvnea7o2sv1vt"
|
"imageName": "sAyoMERxL6JgFfiO22KPb-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqj0nq0001hvnea06r8m3kj",
|
"id": "cmkqj0nq0001hvnea06r8m3kj",
|
||||||
"judul": "Seni Berpikir Kritis",
|
"judul": "Seni Berpikir Kritis",
|
||||||
"deskripsi": "<p>Buku yang membantu pembaca menghindari kesalahan berpikir dalam pengambilan keputusan</p>",
|
"deskripsi": "<p>Buku yang membantu pembaca menghindari kesalahan berpikir dalam pengambilan keputusan</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
"kategoriId": "cmkqb11mc000104jibq87bdzu",
|
||||||
"imageId": "cmkqj0nn0001fvneaufur3nke"
|
"imageName": "WeA-JP2Ks_32fv1k529vj-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqj37w4001kvnea04n9w2bx",
|
"id": "cmkqj37w4001kvnea04n9w2bx",
|
||||||
"judul": "Panduan Shalat Lengkap",
|
"judul": "Panduan Shalat Lengkap",
|
||||||
"deskripsi": "<p>Panduan praktis dan lengkap tentang tata cara shalat sesuai tuntunan</p>",
|
"deskripsi": "<p>Panduan praktis dan lengkap tentang tata cara shalat sesuai tuntunan</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqe7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqe7bdzu",
|
||||||
"imageId": "cmkqj37rg001ivneam29fgayr"
|
"imageName": "pxlHu2kDmIprQqC2PuXaL-mobile.webp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "cmkqj5qp6001nvnea4xhvluz3",
|
"id": "cmkqj5qp6001nvnea4xhvluz3",
|
||||||
"judul": "Cerita Sains untuk Anak",
|
"judul": "Cerita Sains untuk Anak",
|
||||||
"deskripsi": "<p>Cerita edukatif yang mengenalkan sains kepada anak dengan bahasa sederhana</p>",
|
"deskripsi": "<p>Cerita edukatif yang mengenalkan sains kepada anak dengan bahasa sederhana</p>",
|
||||||
"kategoriId": "cmkqb11mc000104jibqh7bdzu",
|
"kategoriId": "cmkqb11mc000104jibqh7bdzu",
|
||||||
"imageId": "cmkqj5ql6001lvnea6p0afr9f"
|
"imageName": "G0iELZb2DhQDCCP5OdzJR-desktop.webp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cml7fq776000104jscnj58sgm",
|
||||||
|
"judul": "Pedagogy of the Oppressed",
|
||||||
|
"deskripsi": "<p>Klasik pemikiran pendidikan kritis; menggali hubungan guru-murid dan peran pendidikan dalam pembebasan sosial</p>",
|
||||||
|
"kategoriId": "cmkqb11mc000104jibq97bdzu",
|
||||||
|
"imageName": "pendidikan-1.webp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cml7fqurm000204js5p60hkym",
|
||||||
|
"judul": "The Courage to Teach",
|
||||||
|
"deskripsi": "<p>Tentang refleksi diri seorang pendidik; cocok untuk pengajar yang ingin lebih dari sekedar “metode mengajar”</p>",
|
||||||
|
"kategoriId": "cmkqb11mc000104jibq97bdzu",
|
||||||
|
"imageName": "pendidikan-2.webp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cml7fqurm000204js5p60hkzn",
|
||||||
|
"judul": "A Brief History of Time",
|
||||||
|
"deskripsi": "<p>Penjelasan kosmologi yang terkenal dunia; sains kompleks dibahas dengan bahasa yang bisa dinikmati pembaca umum</p>",
|
||||||
|
"kategoriId": "cmkqb11mc000104jibqa7bdzu",
|
||||||
|
"imageName": "ilmiah-1.webp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cml7fqurm000204js5p60hkao",
|
||||||
|
"judul": "The Selfish Gene",
|
||||||
|
"deskripsi": "<p>Membawa perspektif baru tentang evolusi melalui “gen” sebagai unit seleksi</p>",
|
||||||
|
"kategoriId": "cmkqb11mc000104jibqa7bdzu",
|
||||||
|
"imageName": "ilmiah-2.webp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cml7fx09c000304jshams3xbg",
|
||||||
|
"judul": "A Little Life",
|
||||||
|
"deskripsi": "<p>Novel yang menggambarkan hidup seorang remaja yang mengalami kehidupan yang sangat sulit</p>",
|
||||||
|
"kategoriId": "cmkqb11mc000104jibqb7bdzu",
|
||||||
|
"imageName": "drama-1.webp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cml7fx09c000304jshams3xch",
|
||||||
|
"judul": "Death of a Salesman",
|
||||||
|
"deskripsi": "<p>Drama teater klasik Amerika tentang harapan, keluarga, dan realitas hidup.</p>",
|
||||||
|
"kategoriId": "cmkqb11mc000104jibqb7bdzu",
|
||||||
|
"imageName": "drama-2.webp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cml7fx09c000304jshams3xdi",
|
||||||
|
"judul": "How Not to Die",
|
||||||
|
"deskripsi": "<p>Panduan berbasis penelitian tentang pola makan untuk mencegah dan menangani penyakit.</p>",
|
||||||
|
"kategoriId": "cmkqb11mc000104jibqg7bdzu",
|
||||||
|
"imageName": "kesehatan-1.webp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cml7fx09c000304jshams3xej",
|
||||||
|
"judul": "The Body Keeps the Score",
|
||||||
|
"deskripsi": "<p>Fokus pada trauma, otak & tubuh; penting untuk memahami kesehatan mental secara mendalam.</p>",
|
||||||
|
"kategoriId": "cmkqb11mc000104jibqg7bdzu",
|
||||||
|
"imageName": "kesehatan-2.webp"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
246
prisma/lib/create_file_share_folder.ts
Normal file
246
prisma/lib/create_file_share_folder.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// import { getValidAuthToken } from "../../src/lib/seafile-auth-service";
|
||||||
|
|
||||||
|
// type CdnItem = {
|
||||||
|
// name: string;
|
||||||
|
// path: string;
|
||||||
|
// cdnUrl: string;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// type DirItem = {
|
||||||
|
// type: "file" | "dir";
|
||||||
|
// name: string;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const BASE_URL = process.env.SEAFILE_BASE_URL!;
|
||||||
|
// const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
||||||
|
|
||||||
|
// // folder yang dishare (RELATIVE, tanpa slash depan)
|
||||||
|
// const DIR_TARGET = "asset-web";
|
||||||
|
|
||||||
|
// // 🔑 TOKEN DIRECTORY SHARE (/d/{token})
|
||||||
|
// const PUBLIC_SHARE_TOKEN = process.env.SEAFILE_PUBLIC_SHARE_TOKEN!;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Ambil list file dari repo (butuh token sekali)
|
||||||
|
// */
|
||||||
|
// async function getDirItems(): Promise<DirItem[]> {
|
||||||
|
// const token = await getValidAuthToken();
|
||||||
|
|
||||||
|
// // Validasi bahwa semua variabel lingkungan telah diatur
|
||||||
|
// if (!BASE_URL) {
|
||||||
|
// throw new Error('SEAFILE_BASE_URL environment variable is not set');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!REPO_ID) {
|
||||||
|
// throw new Error('SEAFILE_REPO_ID environment variable is not set');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Bangun URL dan pastikan valid
|
||||||
|
// const url = `${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// new URL(url); // Ini akan melempar error jika URL tidak valid
|
||||||
|
// } catch (error) {
|
||||||
|
// throw new Error(`Invalid URL constructed: ${url}. Error: ${error}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const res = await fetch(url, {
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Token ${token}`,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!res.ok) {
|
||||||
|
// const text = await res.text();
|
||||||
|
// throw new Error(`Failed get dir items: ${text}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return res.json();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Build PUBLIC CDN URL
|
||||||
|
// */
|
||||||
|
// function buildPublicCdnUrl(fileName: string) {
|
||||||
|
// return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent(
|
||||||
|
// fileName,
|
||||||
|
// )}&raw=1`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Ambil semua PUBLIC CDN URL
|
||||||
|
// */
|
||||||
|
// export async function getAllPublicCdnUrls(): Promise<CdnItem[]> {
|
||||||
|
// const items = await getDirItems();
|
||||||
|
|
||||||
|
// return items
|
||||||
|
// .filter((item) => item.type === "file")
|
||||||
|
// .map((file) => {
|
||||||
|
// const path = `${DIR_TARGET}/${file.name}`;
|
||||||
|
// return {
|
||||||
|
// name: file.name,
|
||||||
|
// path,
|
||||||
|
// cdnUrl: buildPublicCdnUrl(file.name),
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Run langsung (optional)
|
||||||
|
// */
|
||||||
|
// if (import.meta.main) {
|
||||||
|
// const data = await getAllPublicCdnUrls();
|
||||||
|
// console.log(data);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import { getValidAuthToken } from "../../src/lib/seafile-auth-service";
|
||||||
|
|
||||||
|
// type CdnItem = {
|
||||||
|
// name: string;
|
||||||
|
// path: string;
|
||||||
|
// cdnUrl: string;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// type DirItem = {
|
||||||
|
// type: "file" | "dir";
|
||||||
|
// name: string;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // ✅ PAKAI ENV YANG BENAR
|
||||||
|
// const BASE_URL = process.env.SEAFILE_URL!;
|
||||||
|
// const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
||||||
|
// const PUBLIC_SHARE_TOKEN = process.env.SEAFILE_PUBLIC_SHARE_TOKEN!;
|
||||||
|
|
||||||
|
// // folder yang dishare (RELATIVE, TANPA slash depan)
|
||||||
|
// const DIR_TARGET = "asset-web";
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Ambil list file dari repo (token dipakai SEKALI)
|
||||||
|
// */
|
||||||
|
// async function getDirItems(): Promise<DirItem[]> {
|
||||||
|
// if (!BASE_URL || !REPO_ID) {
|
||||||
|
// throw new Error("SEAFILE env not configured correctly");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const token = await getValidAuthToken();
|
||||||
|
|
||||||
|
// const url = `${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`;
|
||||||
|
|
||||||
|
// const res = await fetch(url, {
|
||||||
|
// headers: {
|
||||||
|
// Authorization: `Token ${token}`,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if (!res.ok) {
|
||||||
|
// const text = await res.text();
|
||||||
|
// throw new Error(`Failed get dir items: ${text}`);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return res.json();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Build PUBLIC CDN URL (DIRECTORY SHARE)
|
||||||
|
// */
|
||||||
|
// function buildPublicCdnUrl(fileName: string) {
|
||||||
|
// const fullPath = `/${DIR_TARGET}/${fileName}`;
|
||||||
|
// return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent(
|
||||||
|
// fullPath,
|
||||||
|
// )}&raw=1`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Ambil semua PUBLIC CDN URL
|
||||||
|
// */
|
||||||
|
// export async function getAllPublicCdnUrls(): Promise<CdnItem[]> {
|
||||||
|
// const items = await getDirItems();
|
||||||
|
|
||||||
|
// return items
|
||||||
|
// .filter((item) => item.type === "file")
|
||||||
|
// .map((file) => ({
|
||||||
|
// name: file.name,
|
||||||
|
// path: `${DIR_TARGET}/${file.name}`,
|
||||||
|
// cdnUrl: buildPublicCdnUrl(file.name),
|
||||||
|
// }));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Run langsung
|
||||||
|
// */
|
||||||
|
// if (import.meta.main) {
|
||||||
|
// const data = await getAllPublicCdnUrls();
|
||||||
|
// console.log(data);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
import { getValidAuthToken } from "../../src/lib/seafile-auth-service";
|
||||||
|
type CdnItem = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
cdnUrl: string;
|
||||||
|
};
|
||||||
|
type DirItem = {
|
||||||
|
type: "file" | "dir";
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com";
|
||||||
|
const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
||||||
|
// folder yang dishare (RELATIVE, tanpa slash depan)
|
||||||
|
const DIR_TARGET = "asset-web";
|
||||||
|
// 🔑 TOKEN DIRECTORY SHARE (/d/{token})
|
||||||
|
const PUBLIC_SHARE_TOKEN = "3a9a9ecb5e244f4da8ae";
|
||||||
|
/**
|
||||||
|
* Ambil list file dari repo (butuh token sekali)
|
||||||
|
*/
|
||||||
|
async function getDirItems(): Promise<DirItem[]> {
|
||||||
|
const token = await getValidAuthToken();
|
||||||
|
const res = await fetch(
|
||||||
|
`${BASE_URL}/api2/repos/${REPO_ID}/dir/?p=/${DIR_TARGET}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Token ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`Failed get dir items: ${text}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Build PUBLIC CDN URL
|
||||||
|
*/
|
||||||
|
function buildPublicCdnUrl(fileName: string) {
|
||||||
|
return `${BASE_URL}/d/${PUBLIC_SHARE_TOKEN}/files/?p=${encodeURIComponent(
|
||||||
|
fileName,
|
||||||
|
)}&raw=1`;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Ambil semua PUBLIC CDN URL
|
||||||
|
*/
|
||||||
|
export async function getAllPublicCdnUrls(): Promise<CdnItem[]> {
|
||||||
|
const items = await getDirItems();
|
||||||
|
return items
|
||||||
|
.filter((item) => item.type === "file")
|
||||||
|
.map((file) => {
|
||||||
|
// const path = `${DIR_TARGET}/${file.name}`;
|
||||||
|
const path = `/${file.name}`;
|
||||||
|
return {
|
||||||
|
name: file.name,
|
||||||
|
path,
|
||||||
|
cdnUrl: buildPublicCdnUrl(file.name),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Run langsung (optional)
|
||||||
|
*/
|
||||||
|
if (import.meta.main) {
|
||||||
|
const data = await getAllPublicCdnUrls();
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { getValidAuthToken } from "../../src/lib/seafile-auth-service";
|
||||||
|
|
||||||
type DirItem = {
|
type DirItem = {
|
||||||
type: "file" | "dir";
|
type: "file" | "dir";
|
||||||
name: string;
|
name: string;
|
||||||
@@ -5,7 +7,6 @@ type DirItem = {
|
|||||||
size?: number;
|
size?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TOKEN = process.env.SEAFILE_TOKEN!;
|
|
||||||
const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
||||||
|
|
||||||
// ⛔ PENTING: RELATIVE PATH (tanpa slash depan)
|
// ⛔ PENTING: RELATIVE PATH (tanpa slash depan)
|
||||||
@@ -13,11 +14,12 @@ const DIR_TARGET = "asset-web";
|
|||||||
|
|
||||||
const BASE_URL = process.env.SEAFILE_URL;
|
const BASE_URL = process.env.SEAFILE_URL;
|
||||||
|
|
||||||
const headers = {
|
|
||||||
Authorization: `Token ${TOKEN}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getDirItems(): Promise<DirItem[]> {
|
async function getDirItems(): Promise<DirItem[]> {
|
||||||
|
const token = await getValidAuthToken();
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Token ${token}`,
|
||||||
|
};
|
||||||
|
|
||||||
const res = await fetch(`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`, {
|
const res = await fetch(`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`, {
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
@@ -30,6 +32,11 @@ async function getDirItems(): Promise<DirItem[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getDownloadUrl(filePath: string): Promise<string> {
|
async function getDownloadUrl(filePath: string): Promise<string> {
|
||||||
|
const token = await getValidAuthToken();
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Token ${token}`,
|
||||||
|
};
|
||||||
|
|
||||||
const res = await fetch(
|
const res = await fetch(
|
||||||
`${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}&reuse=1`,
|
`${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}&reuse=1`,
|
||||||
{ headers },
|
{ headers },
|
||||||
|
|||||||
71
prisma/lib/get_shared_images.ts
Normal file
71
prisma/lib/get_shared_images.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//ini code awal cari image by folder di seafile
|
||||||
|
|
||||||
|
|
||||||
|
type CdnItem = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
cdnUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com";
|
||||||
|
const SHARE_ID = "3325e9db2c504ebf9584";
|
||||||
|
|
||||||
|
// https://cld-dkr-makuro-seafile.wibudev.com/d/3a9a9ecb5e244f4da8ae/
|
||||||
|
// https://cld-dkr-makuro-seafile.wibudev.com/d/3a9a9ecb5e244f4da8ae/files/?p=-M_tICRVz6ZxOfvkuHQgU-mobile.webp&raw=1
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build CDN URL langsung (tanpa API, tanpa token)
|
||||||
|
*/
|
||||||
|
export function buildCdnUrl(filePath: string) {
|
||||||
|
// filePath contoh: "banner/home.jpg"
|
||||||
|
return `${BASE_URL}/f/${SHARE_ID}/${filePath}?raw=1`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil daftar file dari PUBLIC SHARE (optional)
|
||||||
|
* Tidak pakai token
|
||||||
|
*/
|
||||||
|
async function getPublicDirItems(path = "/"): Promise<any[]> {
|
||||||
|
const res = await fetch(
|
||||||
|
`${BASE_URL}/api/v2.1/share-links/${SHARE_ID}/dir/?p=${encodeURIComponent(
|
||||||
|
path,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`Failed get public dir items: ${text}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil semua CDN URL dari folder public share
|
||||||
|
*/
|
||||||
|
export async function getAllCdnUrls(
|
||||||
|
dirPath = "/",
|
||||||
|
): Promise<CdnItem[]> {
|
||||||
|
const items = await getPublicDirItems(dirPath);
|
||||||
|
|
||||||
|
return items
|
||||||
|
.filter((item: any) => item.type === "file")
|
||||||
|
.map((file: any) => {
|
||||||
|
const filePath =
|
||||||
|
dirPath === "/"
|
||||||
|
? file.name
|
||||||
|
: `${dirPath.replace(/\/$/, "")}/${file.name}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: file.name,
|
||||||
|
path: filePath,
|
||||||
|
cdnUrl: buildCdnUrl(filePath),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if(import.meta.main) {
|
||||||
|
const allCdnUrls = await getAllCdnUrls();
|
||||||
|
console.log(allCdnUrls);
|
||||||
|
}
|
||||||
33
prisma/lib/get_sharef.ts
Normal file
33
prisma/lib/get_sharef.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com";
|
||||||
|
const ADMIN_TOKEN = process.env.SEAFILE_TOKEN!;
|
||||||
|
const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
||||||
|
|
||||||
|
export async function createFileShareForFolder() {
|
||||||
|
const res = await fetch(`${BASE_URL}/api/v2.1/share-links/`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Token ${ADMIN_TOKEN}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
repo_id: REPO_ID,
|
||||||
|
path: "/asset-web", // FOLDER
|
||||||
|
permission: "r",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
console.log("FILE SHARE LINK:", data);
|
||||||
|
|
||||||
|
// data.link -> https://domain/f/XXXX/
|
||||||
|
// data.token / data.id (tergantung versi)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
await createFileShareForFolder();
|
||||||
|
}
|
||||||
170
prisma/migrations/20260225082505_deploy/migration.sql
Normal file
170
prisma/migrations/20260225082505_deploy/migration.sql
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to alter the column `nama` on the `KategoriPotensi` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
|
||||||
|
- You are about to alter the column `name` on the `PotensiDesa` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(255)`.
|
||||||
|
- You are about to alter the column `kategoriId` on the `PotensiDesa` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(36)`.
|
||||||
|
- A unique constraint covering the columns `[nama]` on the table `KategoriPotensi` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- A unique constraint covering the columns `[name]` on the table `PotensiDesa` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
- Made the column `kategoriId` on table `PotensiDesa` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "DataPerpustakaan" DROP CONSTRAINT "DataPerpustakaan_imageId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "DesaDigital" DROP CONSTRAINT "DesaDigital_imageId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "InfoTekno" DROP CONSTRAINT "InfoTekno_imageId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "KegiatanDesa" DROP CONSTRAINT "KegiatanDesa_imageId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "PengaduanMasyarakat" DROP CONSTRAINT "PengaduanMasyarakat_imageId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "PotensiDesa" DROP CONSTRAINT "PotensiDesa_kategoriId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "ProfileDesaImage" DROP CONSTRAINT "ProfileDesaImage_imageId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CaraMemperolehInformasi" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CaraMemperolehSalinanInformasi" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DaftarInformasiPublik" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DasarHukumPPID" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DataPerpustakaan" ALTER COLUMN "imageId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "DesaDigital" ALTER COLUMN "imageId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "FormulirPermohonanKeberatan" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "InfoTekno" ALTER COLUMN "imageId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "JenisInformasiDiminta" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "JenisKelaminResponden" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "KategoriPotensi" ALTER COLUMN "nama" SET DATA TYPE VARCHAR(100),
|
||||||
|
ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "KategoriPrestasiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "KegiatanDesa" ALTER COLUMN "imageId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LambangDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "MaskotDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PegawaiPPID" ADD COLUMN "deletedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PengaduanMasyarakat" ALTER COLUMN "imageId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PermohonanInformasiPublik" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PilihanRatingResponden" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PosisiOrganisasiPPID" ADD COLUMN "deletedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PotensiDesa" ALTER COLUMN "name" SET DATA TYPE VARCHAR(255),
|
||||||
|
ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT,
|
||||||
|
ALTER COLUMN "kategoriId" SET NOT NULL,
|
||||||
|
ALTER COLUMN "kategoriId" SET DATA TYPE VARCHAR(36);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PrestasiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ProfileDesaImage" ALTER COLUMN "imageId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "ProfilePPID" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Responden" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "SejarahDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UmurResponden" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "VisiMisiDesa" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "VisiMisiPPID" ALTER COLUMN "deletedAt" DROP NOT NULL,
|
||||||
|
ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "KategoriPotensi_nama_key" ON "KategoriPotensi"("nama");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "PotensiDesa_name_key" ON "PotensiDesa"("name");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProfileDesaImage" ADD CONSTRAINT "ProfileDesaImage_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "PotensiDesa" ADD CONSTRAINT "PotensiDesa_kategoriId_fkey" FOREIGN KEY ("kategoriId") REFERENCES "KategoriPotensi"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DesaDigital" ADD CONSTRAINT "DesaDigital_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "InfoTekno" ADD CONSTRAINT "InfoTekno_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "PengaduanMasyarakat" ADD CONSTRAINT "PengaduanMasyarakat_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "KegiatanDesa" ADD CONSTRAINT "KegiatanDesa_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DataPerpustakaan" ADD CONSTRAINT "DataPerpustakaan_imageId_fkey" FOREIGN KEY ("imageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
94
prisma/migrations/20260406032433_init/migration.sql
Normal file
94
prisma/migrations/20260406032433_init/migration.sql
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `realisasi` on the `APBDesItem` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "APBDesItem" DROP COLUMN "realisasi",
|
||||||
|
ADD COLUMN "totalRealisasi" DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||||
|
ALTER COLUMN "selisih" SET DEFAULT 0,
|
||||||
|
ALTER COLUMN "persentase" SET DEFAULT 0;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Berita" ADD COLUMN "linkVideo" VARCHAR(500);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RealisasiItem" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"kode" TEXT,
|
||||||
|
"apbdesItemId" TEXT NOT NULL,
|
||||||
|
"jumlah" DOUBLE PRECISION NOT NULL,
|
||||||
|
"tanggal" DATE NOT NULL,
|
||||||
|
"keterangan" TEXT,
|
||||||
|
"buktiFileId" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
CONSTRAINT "RealisasiItem_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "MusikDesa" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"judul" VARCHAR(255) NOT NULL,
|
||||||
|
"artis" VARCHAR(255) NOT NULL,
|
||||||
|
"deskripsi" TEXT,
|
||||||
|
"durasi" VARCHAR(20) NOT NULL,
|
||||||
|
"audioFileId" TEXT,
|
||||||
|
"coverImageId" TEXT,
|
||||||
|
"genre" VARCHAR(100),
|
||||||
|
"tahunRilis" INTEGER,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
CONSTRAINT "MusikDesa_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_BeritaImages" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_BeritaImages_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RealisasiItem_kode_idx" ON "RealisasiItem"("kode");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RealisasiItem_apbdesItemId_idx" ON "RealisasiItem"("apbdesItemId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "RealisasiItem_tanggal_idx" ON "RealisasiItem"("tanggal");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "MusikDesa_judul_idx" ON "MusikDesa"("judul");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "MusikDesa_artis_idx" ON "MusikDesa"("artis");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_BeritaImages_B_index" ON "_BeritaImages"("B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Berita_kategoriBeritaId_idx" ON "Berita"("kategoriBeritaId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RealisasiItem" ADD CONSTRAINT "RealisasiItem_apbdesItemId_fkey" FOREIGN KEY ("apbdesItemId") REFERENCES "APBDesItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "MusikDesa" ADD CONSTRAINT "MusikDesa_audioFileId_fkey" FOREIGN KEY ("audioFileId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "MusikDesa" ADD CONSTRAINT "MusikDesa_coverImageId_fkey" FOREIGN KEY ("coverImageId") REFERENCES "FileStorage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_BeritaImages" ADD CONSTRAINT "_BeritaImages_A_fkey" FOREIGN KEY ("A") REFERENCES "Berita"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_BeritaImages" ADD CONSTRAINT "_BeritaImages_B_fkey" FOREIGN KEY ("B") REFERENCES "FileStorage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -60,8 +60,9 @@ model FileStorage {
|
|||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
link String
|
link String
|
||||||
category String // "image" / "document" / "other"
|
category String // "image" / "document" / "audio" / "other"
|
||||||
Berita Berita[]
|
Berita Berita[] @relation("BeritaFeaturedImage")
|
||||||
|
BeritaImages Berita[] @relation("BeritaImages")
|
||||||
PotensiDesa PotensiDesa[]
|
PotensiDesa PotensiDesa[]
|
||||||
Posyandu Posyandu[]
|
Posyandu Posyandu[]
|
||||||
StrukturPPID StrukturPPID[]
|
StrukturPPID StrukturPPID[]
|
||||||
@@ -102,6 +103,9 @@ model FileStorage {
|
|||||||
|
|
||||||
ArtikelKesehatan ArtikelKesehatan[]
|
ArtikelKesehatan ArtikelKesehatan[]
|
||||||
StrukturBumDes StrukturBumDes[]
|
StrukturBumDes StrukturBumDes[]
|
||||||
|
|
||||||
|
MusikDesaAudio MusikDesa[] @relation("MusikAudioFile")
|
||||||
|
MusikDesaCover MusikDesa[] @relation("MusikCoverImage")
|
||||||
}
|
}
|
||||||
|
|
||||||
//========================================= MENU LANDING PAGE ========================================= //
|
//========================================= MENU LANDING PAGE ========================================= //
|
||||||
@@ -205,16 +209,22 @@ model APBDesItem {
|
|||||||
kode String // contoh: "4", "4.1", "4.1.2"
|
kode String // contoh: "4", "4.1", "4.1.2"
|
||||||
uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
|
uraian String // nama item, contoh: "Pendapatan Asli Desa", "Hasil Usaha"
|
||||||
anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
|
anggaran Float // dalam satuan Rupiah (bisa DECIMAL di DB, tapi Float umum di TS/JS)
|
||||||
realisasi Float
|
tipe String? // "pendapatan" | "belanja" | "pembiayaan" | null
|
||||||
selisih Float // realisasi - anggaran
|
|
||||||
persentase Float
|
|
||||||
tipe String? // (realisasi / anggaran) * 100
|
|
||||||
level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
|
level Int // 1 = kelompok utama, 2 = sub-kelompok, 3 = detail
|
||||||
parentId String? // untuk relasi hierarki
|
parentId String? // untuk relasi hierarki
|
||||||
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
|
parent APBDesItem? @relation("APBDesItemParent", fields: [parentId], references: [id])
|
||||||
children APBDesItem[] @relation("APBDesItemParent")
|
children APBDesItem[] @relation("APBDesItemParent")
|
||||||
apbdesId String
|
apbdesId String
|
||||||
apbdes APBDes @relation(fields: [apbdesId], references: [id])
|
apbdes APBDes @relation(fields: [apbdesId], references: [id])
|
||||||
|
|
||||||
|
// Field kalkulasi (auto-calculated dari realisasi items)
|
||||||
|
totalRealisasi Float @default(0) // Sum dari semua realisasi
|
||||||
|
selisih Float @default(0) // totalRealisasi - anggaran
|
||||||
|
persentase Float @default(0) // (totalRealisasi / anggaran) * 100
|
||||||
|
|
||||||
|
// Relasi ke realisasi items
|
||||||
|
realisasiItems RealisasiItem[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
@@ -225,6 +235,28 @@ model APBDesItem {
|
|||||||
@@index([apbdesId])
|
@@index([apbdesId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model baru untuk multiple realisasi per item
|
||||||
|
model RealisasiItem {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
kode String? // Kode realisasi, mirip dengan APBDesItem
|
||||||
|
apbdesItemId String
|
||||||
|
apbdesItem APBDesItem @relation(fields: [apbdesItemId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
jumlah Float // Jumlah realisasi dalam Rupiah
|
||||||
|
tanggal DateTime @db.Date // Tanggal realisasi
|
||||||
|
keterangan String? @db.Text // Keterangan tambahan (opsional)
|
||||||
|
buktiFileId String? // FileStorage ID untuk bukti/foto (opsional)
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
@@index([kode])
|
||||||
|
@@index([apbdesItemId])
|
||||||
|
@@index([tanggal])
|
||||||
|
}
|
||||||
|
|
||||||
//========================================= PRESTASI DESA ========================================= //
|
//========================================= PRESTASI DESA ========================================= //
|
||||||
model PrestasiDesa {
|
model PrestasiDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
@@ -236,7 +268,7 @@ model PrestasiDesa {
|
|||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +277,7 @@ model KategoriPrestasiDesa {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PrestasiDesa PrestasiDesa[]
|
PrestasiDesa PrestasiDesa[]
|
||||||
}
|
}
|
||||||
@@ -263,7 +295,7 @@ model Responden {
|
|||||||
kelompokUmurId String
|
kelompokUmurId String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +304,7 @@ model JenisKelaminResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -282,7 +314,7 @@ model PilihanRatingResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -292,7 +324,7 @@ model UmurResponden {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
Responden Responden[]
|
Responden Responden[]
|
||||||
}
|
}
|
||||||
@@ -326,6 +358,7 @@ model PosisiOrganisasiPPID {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
@@ -345,6 +378,7 @@ model PegawaiPPID {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
posisi PosisiOrganisasiPPID @relation(fields: [posisiId], references: [id])
|
||||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||||
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
StrukturOrganisasiPPID StrukturOrganisasiPPID[]
|
||||||
@@ -370,7 +404,7 @@ model VisiMisiPPID {
|
|||||||
misi String @db.Text
|
misi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +415,7 @@ model DasarHukumPPID {
|
|||||||
content String @db.Text
|
content String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +432,7 @@ model ProfilePPID {
|
|||||||
imageId String?
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +444,7 @@ model DaftarInformasiPublik {
|
|||||||
tanggal DateTime @db.Date
|
tanggal DateTime @db.Date
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,7 +465,7 @@ model PermohonanInformasiPublik {
|
|||||||
caraMemperolehSalinanInformasiId String?
|
caraMemperolehSalinanInformasiId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,7 +474,7 @@ model JenisInformasiDiminta {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -450,7 +484,7 @@ model CaraMemperolehInformasi {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -460,7 +494,7 @@ model CaraMemperolehSalinanInformasi {
|
|||||||
name String @unique
|
name String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
PermohonanInformasiPublik PermohonanInformasiPublik[]
|
||||||
}
|
}
|
||||||
@@ -474,7 +508,7 @@ model FormulirPermohonanKeberatan {
|
|||||||
alasan String
|
alasan String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +565,7 @@ model SejarahDesa {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -541,7 +575,7 @@ model VisiMisiDesa {
|
|||||||
misi String @db.Text
|
misi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +585,7 @@ model LambangDesa {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,7 +596,7 @@ model MaskotDesa {
|
|||||||
images ProfileDesaImage[]
|
images ProfileDesaImage[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,15 +641,19 @@ model Berita {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
judul String
|
judul String
|
||||||
deskripsi String
|
deskripsi String
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation("BeritaFeaturedImage", fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
|
images FileStorage[] @relation("BeritaImages")
|
||||||
content String @db.Text
|
content String @db.Text
|
||||||
|
linkVideo String? @db.VarChar(500)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
kategoriBerita KategoriBerita? @relation(fields: [kategoriBeritaId], references: [id])
|
kategoriBerita KategoriBerita? @relation(fields: [kategoriBeritaId], references: [id])
|
||||||
kategoriBeritaId String?
|
kategoriBeritaId String?
|
||||||
|
|
||||||
|
@@index([kategoriBeritaId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model KategoriBerita {
|
model KategoriBerita {
|
||||||
@@ -631,25 +669,25 @@ model KategoriBerita {
|
|||||||
// ========================================= POTENSI DESA ========================================= //
|
// ========================================= POTENSI DESA ========================================= //
|
||||||
model PotensiDesa {
|
model PotensiDesa {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String @unique @db.VarChar(255)
|
||||||
deskripsi String
|
deskripsi String @db.Text
|
||||||
kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id])
|
kategori KategoriPotensi? @relation(fields: [kategoriId], references: [id])
|
||||||
kategoriId String?
|
kategoriId String @db.VarChar(36)
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
content String @db.Text
|
content String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
model KategoriPotensi {
|
model KategoriPotensi {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
nama String
|
nama String @unique @db.VarChar(100)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
PotensiDesa PotensiDesa[]
|
PotensiDesa PotensiDesa[]
|
||||||
}
|
}
|
||||||
@@ -1659,8 +1697,8 @@ model DesaDigital {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
@@ -1710,8 +1748,8 @@ model InfoTekno {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
@@ -1766,8 +1804,8 @@ model PengaduanMasyarakat {
|
|||||||
nik String
|
nik String
|
||||||
judulPengaduan String
|
judulPengaduan String
|
||||||
lokasiKejadian String
|
lokasiKejadian String
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
deskripsiPengaduan String @db.Text
|
deskripsiPengaduan String @db.Text
|
||||||
jenisPengaduan JenisPengaduan @relation(fields: [jenisPengaduanId], references: [id])
|
jenisPengaduan JenisPengaduan @relation(fields: [jenisPengaduanId], references: [id])
|
||||||
jenisPengaduanId String
|
jenisPengaduanId String
|
||||||
@@ -1848,8 +1886,8 @@ model KegiatanDesa {
|
|||||||
tanggal DateTime
|
tanggal DateTime
|
||||||
lokasi String
|
lokasi String
|
||||||
partisipan Int
|
partisipan Int
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
@@ -2133,8 +2171,8 @@ model DataPerpustakaan {
|
|||||||
deskripsi String @db.Text
|
deskripsi String @db.Text
|
||||||
kategori KategoriBuku @relation(fields: [kategoriId], references: [id])
|
kategori KategoriBuku @relation(fields: [kategoriId], references: [id])
|
||||||
kategoriId String
|
kategoriId String
|
||||||
image FileStorage @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String
|
imageId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletedAt DateTime @default(now())
|
deletedAt DateTime @default(now())
|
||||||
@@ -2261,3 +2299,25 @@ model UserMenuAccess {
|
|||||||
|
|
||||||
@@unique([userId, menuId]) // Satu user tidak bisa punya akses menu yang sama dua kali
|
@@unique([userId, menuId]) // Satu user tidak bisa punya akses menu yang sama dua kali
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================= MUSIK DESA ========================================= //
|
||||||
|
model MusikDesa {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
judul String @db.VarChar(255)
|
||||||
|
artis String @db.VarChar(255)
|
||||||
|
deskripsi String? @db.Text
|
||||||
|
durasi String @db.VarChar(20) // format: "MM:SS"
|
||||||
|
audioFile FileStorage? @relation("MusikAudioFile", fields: [audioFileId], references: [id])
|
||||||
|
audioFileId String?
|
||||||
|
coverImage FileStorage? @relation("MusikCoverImage", fields: [coverImageId], references: [id])
|
||||||
|
coverImageId String?
|
||||||
|
genre String? @db.VarChar(100)
|
||||||
|
tahunRilis Int?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
deletedAt DateTime?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
|
||||||
|
@@index([judul])
|
||||||
|
@@index([artis])
|
||||||
|
}
|
||||||
|
|||||||
1771
prisma/seed.ts
1771
prisma/seed.ts
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user