Compare commits
1 Commits
deploy/stg
...
nico/test-
| Author | SHA1 | Date | |
|---|---|---|---|
| 091c33a73c |
@@ -1,47 +0,0 @@
|
|||||||
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
44
.env.example
@@ -1,44 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/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();
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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
Normal file
219
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
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
56
.github/workflows/docker-publish.yml
vendored
@@ -1,56 +0,0 @@
|
|||||||
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
106
.github/workflows/publish.yml
vendored
@@ -1,106 +0,0 @@
|
|||||||
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
60
.github/workflows/re-pull.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
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
26
.github/workflows/script/notify.sh
vendored
@@ -1,26 +0,0 @@
|
|||||||
#!/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
120
.github/workflows/script/re-pull.sh
vendored
@@ -1,120 +0,0 @@
|
|||||||
#!/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
Normal file
55
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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,12 +29,7 @@ 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
|
||||||
@@ -52,6 +47,9 @@ next-env.d.ts
|
|||||||
# cache
|
# cache
|
||||||
/cache
|
/cache
|
||||||
|
|
||||||
|
.github/
|
||||||
|
|
||||||
|
.env.*
|
||||||
|
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"playwright-mcp": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": [
|
|
||||||
"-y",
|
|
||||||
"playwright-mcp@latest"
|
|
||||||
],
|
|
||||||
"timeout": 60000
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"$version": 3
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"playwright-mcp": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["-y", "playwright-mcp@latest"],
|
|
||||||
"timeout": 60000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": []
|
|
||||||
}
|
|
||||||
167
AGENTS.md
167
AGENTS.md
@@ -1,167 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
This file contains essential information for agentic coding agents working in the desa-darmasaba repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Desa Darmasaba is a Next.js 15 application for village management services in Badung, Bali. It uses:
|
|
||||||
- **Framework**: Next.js 15 with App Router
|
|
||||||
- **Language**: TypeScript with strict mode
|
|
||||||
- **Styling**: Mantine UI components with custom CSS
|
|
||||||
- **Backend**: Elysia.js API server integrated with Next.js
|
|
||||||
- **Database**: PostgreSQL with Prisma ORM
|
|
||||||
- **State Management**: Jotai for global state
|
|
||||||
- **Authentication**: JWT with iron-session
|
|
||||||
|
|
||||||
## Build Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Development
|
|
||||||
npm run dev
|
|
||||||
|
|
||||||
# Production build
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# Start production server
|
|
||||||
npm start
|
|
||||||
|
|
||||||
# Database seeding
|
|
||||||
bun run prisma/seed.ts
|
|
||||||
|
|
||||||
# Linting (ESLint)
|
|
||||||
npx eslint .
|
|
||||||
|
|
||||||
# Type checking
|
|
||||||
npx tsc --noEmit
|
|
||||||
|
|
||||||
# Prisma operations
|
|
||||||
npx prisma generate
|
|
||||||
npx prisma db push
|
|
||||||
npx prisma studio
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Tests
|
|
||||||
|
|
||||||
Currently no test framework is configured. When adding tests:
|
|
||||||
- Set up test scripts in package.json
|
|
||||||
- Consider Jest or Vitest for unit testing
|
|
||||||
- Use Playwright for E2E testing
|
|
||||||
- Update this section with specific test commands
|
|
||||||
|
|
||||||
## Code Style Guidelines
|
|
||||||
|
|
||||||
### Imports
|
|
||||||
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
|
||||||
- Group imports: external libraries first, then internal modules
|
|
||||||
- Keep import statements organized and remove unused imports
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// External libraries
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Button, Stack } from '@mantine/core'
|
|
||||||
|
|
||||||
// Internal modules
|
|
||||||
import ApiFetch from '@/lib/api-fetch'
|
|
||||||
import { MyComponent } from '@/components/my-component'
|
|
||||||
```
|
|
||||||
|
|
||||||
### TypeScript Configuration
|
|
||||||
- Strict mode enabled (`"strict": true`)
|
|
||||||
- Target: ES2017
|
|
||||||
- Module resolution: bundler
|
|
||||||
- Path alias: `@/*` maps to `./src/*`
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
|
|
||||||
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
|
|
||||||
- **Variables/Functions**: camelCase
|
|
||||||
- **Constants**: UPPER_SNAKE_CASE
|
|
||||||
- **Database Models**: PascalCase (Prisma convention)
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Use try-catch blocks for async operations
|
|
||||||
- Implement proper error boundaries in React components
|
|
||||||
- Log errors appropriately without exposing sensitive data
|
|
||||||
- Use Zod for runtime validation and type safety
|
|
||||||
|
|
||||||
### API Structure
|
|
||||||
- Backend uses Elysia.js with TypeScript
|
|
||||||
- API routes are in `src/app/api/[[...slugs]]/` directory
|
|
||||||
- Use treaty client for type-safe API calls
|
|
||||||
- Follow RESTful conventions for endpoints
|
|
||||||
- Include proper HTTP status codes and error responses
|
|
||||||
|
|
||||||
### Database Operations
|
|
||||||
- Use Prisma client from `@/lib/prisma.ts`
|
|
||||||
- Database connection includes graceful shutdown handling
|
|
||||||
- Use transactions for complex operations
|
|
||||||
- Implement proper error handling for database queries
|
|
||||||
|
|
||||||
### Component Guidelines
|
|
||||||
- Use functional components with hooks
|
|
||||||
- Implement proper prop types with TypeScript interfaces
|
|
||||||
- Use Mantine components for UI consistency
|
|
||||||
- Follow atomic design principles when possible
|
|
||||||
- Add loading states and error states for async operations
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
- Use Jotai atoms for global state
|
|
||||||
- Keep local state in components when possible
|
|
||||||
- Use React Query (SWR) for server state caching
|
|
||||||
- Implement optimistic updates for better UX
|
|
||||||
|
|
||||||
### Styling
|
|
||||||
- Primary: Mantine UI components
|
|
||||||
- Use Mantine theme system for customization
|
|
||||||
- Custom CSS should be minimal and scoped
|
|
||||||
- Follow responsive design principles
|
|
||||||
- Use semantic HTML5 elements
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
- Use `.env.local` for development
|
|
||||||
- Prefix public variables with `NEXT_PUBLIC_`
|
|
||||||
- Never commit environment files to version control
|
|
||||||
- Use proper typing for environment variables
|
|
||||||
|
|
||||||
### File Organization
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── app/ # Next.js app router pages
|
|
||||||
├── components/ # Reusable React components
|
|
||||||
├── lib/ # Utility functions and configurations
|
|
||||||
├── state/ # Jotai atoms and state management
|
|
||||||
├── types/ # TypeScript type definitions
|
|
||||||
└── con/ # Constants and static data
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security Practices
|
|
||||||
- Validate all user inputs with Zod schemas
|
|
||||||
- Use JWT tokens for authentication
|
|
||||||
- Implement proper CORS configuration
|
|
||||||
- Never expose database credentials or API keys
|
|
||||||
- Use HTTPS in production
|
|
||||||
- Implement rate limiting for sensitive endpoints
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
- Use Next.js Image optimization
|
|
||||||
- Implement proper caching strategies
|
|
||||||
- Use React.memo for expensive components
|
|
||||||
- Optimize bundle size with dynamic imports
|
|
||||||
- Use Prisma query optimization
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. Always run type checking before committing: `npx tsc --noEmit`
|
|
||||||
2. Run linting to catch style issues: `npx eslint .`
|
|
||||||
3. Test database changes with `npx prisma db push`
|
|
||||||
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
|
||||||
5. Check environment variables are properly configured
|
|
||||||
6. Verify responsive design on different screen sizes
|
|
||||||
|
|
||||||
## Important Notes
|
|
||||||
|
|
||||||
- The application uses a custom Elysia.js server integrated with Next.js API routes
|
|
||||||
- Image uploads are handled through `/api/upl-img-single` endpoint
|
|
||||||
- Database seeding is done with Bun runtime
|
|
||||||
- The app supports Indonesian locale (id_ID) for SEO and content
|
|
||||||
- CORS is configured to allow cross-origin requests during development
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# 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.).
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
# 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)
|
|
||||||
74
Dockerfile
74
Dockerfile
@@ -1,74 +0,0 @@
|
|||||||
# ==============================
|
|
||||||
# Stage 1: Builder
|
|
||||||
# ==============================
|
|
||||||
FROM oven/bun:1-debian AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libc6 \
|
|
||||||
git \
|
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY package.json bun.lockb* ./
|
|
||||||
|
|
||||||
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|
||||||
|
|
||||||
RUN bun install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN cp .env.example .env || true
|
|
||||||
|
|
||||||
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
|
|
||||||
RUN bunx prisma generate
|
|
||||||
|
|
||||||
# Generate API types (opsional)
|
|
||||||
RUN bun run gen:api || echo "tidak ada gen api"
|
|
||||||
|
|
||||||
RUN bun run build
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# Stage 2: Runner (Production)
|
|
||||||
# ==============================
|
|
||||||
FROM oven/bun:1-debian AS runner
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
|
|
||||||
ENV PORT=3000
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN groupadd --system --gid 1001 nodejs \
|
|
||||||
&& useradd --system --uid 1001 --gid nodejs nextjs
|
|
||||||
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/next.config.* ./
|
|
||||||
COPY --chmod=755 docker-entrypoint.sh ./docker-entrypoint.sh
|
|
||||||
|
|
||||||
# Create uploads directory with proper permissions
|
|
||||||
RUN mkdir -p /app/uploads && chown nextjs:nodejs /app/uploads
|
|
||||||
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
# Persistent storage for uploaded files
|
|
||||||
VOLUME ["/app/uploads"]
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
CMD ["/app/docker-entrypoint.sh"]
|
|
||||||
62
GEMINI.md
62
GEMINI.md
@@ -1,62 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
# 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
|
|
||||||
235
QWEN.md
235
QWEN.md
@@ -1,235 +0,0 @@
|
|||||||
# Desa Darmasaba - Village Management System
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Desa Darmasaba is a comprehensive Next.js 15 application designed for village management services in Darmasaba, Badung, Bali. The application serves as a digital platform for government services, public information, and community engagement. It features multiple sections including PPID (Public Information Disclosure), health services, security, education, environment, economy, innovation, and more.
|
|
||||||
|
|
||||||
### Key Technologies
|
|
||||||
- **Framework**: Next.js 15 with App Router
|
|
||||||
- **Language**: TypeScript with strict mode
|
|
||||||
- **Styling**: Mantine UI components with custom CSS
|
|
||||||
- **Backend**: Elysia.js API server integrated with Next.js
|
|
||||||
- **Database**: PostgreSQL with Prisma ORM
|
|
||||||
- **State Management**: Valtio for global state
|
|
||||||
- **Authentication**: JWT with iron-session
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
The application follows a modular architecture with:
|
|
||||||
- A main frontend built with Next.js and Mantine UI
|
|
||||||
- An integrated Elysia.js API server for backend operations
|
|
||||||
- Prisma ORM for database interactions
|
|
||||||
- File storage integration with Seafile
|
|
||||||
- Multiple domain-specific modules (PPID, health, security, education, etc.)
|
|
||||||
|
|
||||||
## Building and Running
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Node.js (with Bun runtime)
|
|
||||||
- PostgreSQL database
|
|
||||||
- Seafile server for file storage
|
|
||||||
|
|
||||||
### Setup Instructions
|
|
||||||
1. Install dependencies:
|
|
||||||
```bash
|
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Set up environment variables in `.env.local`:
|
|
||||||
```
|
|
||||||
DATABASE_URL=your_postgresql_connection_string
|
|
||||||
SEAFILE_TOKEN=your_seafile_token
|
|
||||||
SEAFILE_REPO_ID=your_seafile_repo_id
|
|
||||||
SEAFILE_BASE_URL=your_seafile_base_url
|
|
||||||
SEAFILE_PUBLIC_SHARE_TOKEN=your_seafile_public_share_token
|
|
||||||
SEAFILE_URL=your_seafile_api_url
|
|
||||||
WIBU_UPLOAD_DIR=your_upload_directory
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Generate Prisma client:
|
|
||||||
```bash
|
|
||||||
bunx prisma generate
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Push database schema:
|
|
||||||
```bash
|
|
||||||
bunx prisma db push
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Seed the database:
|
|
||||||
```bash
|
|
||||||
bun run prisma/seed.ts
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Run the development server:
|
|
||||||
```bash
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Available Scripts
|
|
||||||
- `bun run dev` - Start development server
|
|
||||||
- `bun run build` - Build for production
|
|
||||||
- `bun run start` - Start production server
|
|
||||||
- `bun run prisma/seed.ts` - Run database seeding
|
|
||||||
- `bunx prisma generate` - Generate Prisma client
|
|
||||||
- `bunx prisma db push` - Push schema changes to database
|
|
||||||
- `bunx prisma studio` - Open Prisma Studio GUI
|
|
||||||
|
|
||||||
## Development Conventions
|
|
||||||
|
|
||||||
### Code Structure
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── app/ # Next.js app router pages
|
|
||||||
│ ├── admin/ # Admin dashboard pages
|
|
||||||
│ ├── api/ # API routes with Elysia.js
|
|
||||||
│ ├── darmasaba/ # Public-facing village pages
|
|
||||||
│ └── ...
|
|
||||||
├── con/ # Constants and configuration
|
|
||||||
├── hooks/ # React hooks
|
|
||||||
├── lib/ # Utility functions and configurations
|
|
||||||
├── middlewares/ # Next.js middleware
|
|
||||||
├── state/ # Global state management
|
|
||||||
├── store/ # Additional state management
|
|
||||||
├── types/ # TypeScript type definitions
|
|
||||||
└── utils/ # Utility functions
|
|
||||||
```
|
|
||||||
|
|
||||||
### Import Conventions
|
|
||||||
- Use absolute imports with `@/` alias (configured in tsconfig.json)
|
|
||||||
- Group imports: external libraries first, then internal modules
|
|
||||||
- Keep import statements organized and remove unused imports
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// External libraries
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { Button, Stack } from '@mantine/core'
|
|
||||||
|
|
||||||
// Internal modules
|
|
||||||
import ApiFetch from '@/lib/api-fetch'
|
|
||||||
import { MyComponent } from '@/components/my-component'
|
|
||||||
```
|
|
||||||
|
|
||||||
### TypeScript Configuration
|
|
||||||
- Strict mode enabled (`"strict": true`)
|
|
||||||
- Target: ES2017
|
|
||||||
- Module resolution: bundler
|
|
||||||
- Path alias: `@/*` maps to `./src/*`
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
- **Components**: PascalCase (e.g., `UploadImage.tsx`)
|
|
||||||
- **Files**: kebab-case for utilities (e.g., `api-fetch.ts`)
|
|
||||||
- **Variables/Functions**: camelCase
|
|
||||||
- **Constants**: UPPER_SNAKE_CASE
|
|
||||||
- **Database Models**: PascalCase (Prisma convention)
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Use try-catch blocks for async operations
|
|
||||||
- Implement proper error boundaries in React components
|
|
||||||
- Log errors appropriately without exposing sensitive data
|
|
||||||
- Use Zod for runtime validation and type safety
|
|
||||||
|
|
||||||
### API Structure
|
|
||||||
- Backend uses Elysia.js with TypeScript
|
|
||||||
- API routes are in `src/app/api/[[...slugs]]/` directory
|
|
||||||
- Use treaty client for type-safe API calls
|
|
||||||
- Follow RESTful conventions for endpoints
|
|
||||||
- Include proper HTTP status codes and error responses
|
|
||||||
|
|
||||||
### Database Operations
|
|
||||||
- Use Prisma client from `@/lib/prisma.ts`
|
|
||||||
- Database connection includes graceful shutdown handling
|
|
||||||
- Use transactions for complex operations
|
|
||||||
- Implement proper error handling for database queries
|
|
||||||
|
|
||||||
### Component Guidelines
|
|
||||||
- Use functional components with hooks
|
|
||||||
- Implement proper prop types with TypeScript interfaces
|
|
||||||
- Use Mantine components for UI consistency
|
|
||||||
- Follow atomic design principles when possible
|
|
||||||
- Add loading states and error states for async operations
|
|
||||||
|
|
||||||
### State Management
|
|
||||||
- Use Valtio proxies for global state
|
|
||||||
- Keep local state in components when possible
|
|
||||||
- Use SWR for server state caching
|
|
||||||
- Implement optimistic updates for better UX
|
|
||||||
|
|
||||||
### Styling
|
|
||||||
- Primary: Mantine UI components
|
|
||||||
- Use Mantine theme system for customization
|
|
||||||
- Custom CSS should be minimal and scoped
|
|
||||||
- Follow responsive design principles
|
|
||||||
- Use semantic HTML5 elements
|
|
||||||
|
|
||||||
### Security Practices
|
|
||||||
- Validate all user inputs with Zod schemas
|
|
||||||
- Use JWT tokens for authentication
|
|
||||||
- Implement proper CORS configuration
|
|
||||||
- Never expose database credentials or API keys
|
|
||||||
- Use HTTPS in production
|
|
||||||
- Implement rate limiting for sensitive endpoints
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
- Use Next.js Image optimization
|
|
||||||
- Implement proper caching strategies
|
|
||||||
- Use React.memo for expensive components
|
|
||||||
- Optimize bundle size with dynamic imports
|
|
||||||
- Use Prisma query optimization
|
|
||||||
|
|
||||||
## Domain Modules
|
|
||||||
|
|
||||||
The application is organized into several domain modules:
|
|
||||||
|
|
||||||
1. **PPID (Public Information Disclosure)**: Profile, structure, information requests, legal basis
|
|
||||||
2. **Health**: Health facilities, programs, emergency response, disease information
|
|
||||||
3. **Security**: Community security, emergency contacts, crime prevention
|
|
||||||
4. **Education**: Schools, scholarships, educational programs
|
|
||||||
5. **Economy**: Local markets, BUMDes, employment data
|
|
||||||
6. **Environment**: Environmental data, conservation, waste management
|
|
||||||
7. **Innovation**: Digital services, innovation programs
|
|
||||||
8. **Culture**: Village traditions, music, cultural preservation
|
|
||||||
|
|
||||||
Each module has its own section in both the admin panel and public-facing areas.
|
|
||||||
|
|
||||||
## File Storage Integration
|
|
||||||
|
|
||||||
The application integrates with Seafile for file storage, with specific handling for:
|
|
||||||
- Images and documents
|
|
||||||
- Public sharing capabilities
|
|
||||||
- CDN URL generation
|
|
||||||
- Batch processing of assets
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Currently no formal test framework is configured. When adding tests:
|
|
||||||
- Consider Jest or Vitest for unit testing
|
|
||||||
- Use Playwright for E2E testing
|
|
||||||
- Update this section with specific test commands
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
The application includes deployment scripts in the `NOTE.md` file that outline:
|
|
||||||
- Automated deployment with GitHub API integration
|
|
||||||
- Environment-specific configurations
|
|
||||||
- PM2 process management
|
|
||||||
- Release management with versioning
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Common issues and solutions:
|
|
||||||
- **API endpoints returning 404**: Check that environment variables are properly configured
|
|
||||||
- **Database connection errors**: Verify DATABASE_URL in environment variables
|
|
||||||
- **File upload issues**: Ensure Seafile integration is properly configured
|
|
||||||
- **Build failures**: Run `bunx prisma generate` before building
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. Always run type checking before committing: `bunx tsc --noEmit`
|
|
||||||
2. Run linting to catch style issues: `bun run eslint .`
|
|
||||||
3. Test database changes with `bunx prisma db push`
|
|
||||||
4. Use the integrated Swagger docs at `/api/docs` for API testing
|
|
||||||
5. Check environment variables are properly configured
|
|
||||||
6. Verify responsive design on different screen sizes
|
|
||||||
|
|
||||||
## Qwen Added Memories
|
|
||||||
- **GitHub Workflows**: Project ini memiliki workflow GitHub Action untuk deployment. User akan menangani workflow secara manual di GitHub.
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { setupServer } from 'msw/node';
|
|
||||||
import { handlers } from './handlers';
|
|
||||||
|
|
||||||
export const server = setupServer(...handlers);
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import '@testing-library/jest-dom';
|
|
||||||
import { server } from './mocks/server';
|
|
||||||
import { beforeAll, afterEach, afterAll } from 'vitest';
|
|
||||||
|
|
||||||
beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' }));
|
|
||||||
afterEach(() => server.resetHandlers());
|
|
||||||
afterAll(() => server.close());
|
|
||||||
49
biome.json
49
biome.json
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
|
|
||||||
"vcs": {
|
|
||||||
"enabled": true,
|
|
||||||
"clientKind": "git",
|
|
||||||
"useIgnoreFile": true
|
|
||||||
},
|
|
||||||
"files": {
|
|
||||||
"ignoreUnknown": false,
|
|
||||||
"experimentalScannerIgnores": [
|
|
||||||
"node_modules",
|
|
||||||
".next",
|
|
||||||
"out",
|
|
||||||
"public"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"indentWidth": 2,
|
|
||||||
"lineWidth": 100
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"correctness": {
|
|
||||||
"noUnusedVariables": "warn",
|
|
||||||
"noUnusedImports": "warn"
|
|
||||||
},
|
|
||||||
"suspicious": {
|
|
||||||
"noExplicitAny": "warn"
|
|
||||||
},
|
|
||||||
"style": {
|
|
||||||
"noNonNullAssertion": "warn"
|
|
||||||
},
|
|
||||||
"complexity": {
|
|
||||||
"noForEach": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"quoteStyle": "single",
|
|
||||||
"trailingCommas": "all",
|
|
||||||
"semicolons": "always"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
169
darkMode.md
169
darkMode.md
@@ -1,169 +0,0 @@
|
|||||||
# 🌙 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.
|
|
||||||
@@ -1,553 +0,0 @@
|
|||||||
# 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,13 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "🔄 Running database migrations..."
|
|
||||||
cd /app
|
|
||||||
bunx prisma migrate deploy || {
|
|
||||||
echo "❌ Migration failed!"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "✅ Migrations completed successfully"
|
|
||||||
|
|
||||||
echo "🚀 Starting application..."
|
|
||||||
exec bun start
|
|
||||||
@@ -11,11 +11,6 @@ const compat = new FlatCompat({
|
|||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
|||||||
99
gambar.ttx
99
gambar.ttx
@@ -1,99 +0,0 @@
|
|||||||
type DirItem = {
|
|
||||||
type: "file" | "dir";
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
size?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
// type FileDownloadResponse = {
|
|
||||||
// url: string;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const TOKEN = "20a19f4a04032215d50ce53292e6abdd38b9f806";
|
|
||||||
// const REPO_ID = "8814bfe1-30d5-4e77-ab36-3122fa59a022";
|
|
||||||
// const DIR_TARGET = "image";
|
|
||||||
|
|
||||||
// const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
|
|
||||||
|
|
||||||
const TOKEN = process.env.SEAFILE_TOKEN!;
|
|
||||||
const REPO_ID = process.env.SEAFILE_REPO_ID!;
|
|
||||||
|
|
||||||
// ⛔ PENTING: RELATIVE PATH (tanpa slash depan)
|
|
||||||
const DIR_TARGET = "asset-web";
|
|
||||||
|
|
||||||
const BASE_URL = "https://cld-dkr-makuro-seafile.wibudev.com/api2";
|
|
||||||
|
|
||||||
const headers = {
|
|
||||||
Authorization: `Token ${TOKEN}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ambil list file di directory
|
|
||||||
*/
|
|
||||||
async function getDirItems(): Promise<DirItem[]> {
|
|
||||||
const res = await fetch(
|
|
||||||
`${BASE_URL}/repos/${REPO_ID}/dir/?p=${DIR_TARGET}`,
|
|
||||||
{ headers }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Failed get dir items: ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ambil download URL file
|
|
||||||
*/
|
|
||||||
async function getDownloadUrl(filePath: string): Promise<string> {
|
|
||||||
|
|
||||||
|
|
||||||
const res = await fetch(
|
|
||||||
`${BASE_URL}/repos/${REPO_ID}/file/?p=${encodeURIComponent(filePath)}`,
|
|
||||||
{ headers }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`Failed get file url: ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ambil semua download URL dari target dir
|
|
||||||
*/
|
|
||||||
async function getAllDownloadUrls() {
|
|
||||||
const items = await getDirItems();
|
|
||||||
|
|
||||||
const files = items.filter((item) => item.type === "file");
|
|
||||||
|
|
||||||
const results = await Promise.all(
|
|
||||||
files.map(async (file) => {
|
|
||||||
const filePath = `${DIR_TARGET}/${file.name}`;
|
|
||||||
const url = await getDownloadUrl(filePath);
|
|
||||||
return {
|
|
||||||
name: file.name,
|
|
||||||
path: filePath,
|
|
||||||
downloadUrl: url,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// contoh eksekusi
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
console.log("ambil gambar")
|
|
||||||
const urls = await getAllDownloadUrls();
|
|
||||||
await Bun.write("list_image2.json", JSON.stringify(urls))
|
|
||||||
console.log("selesai !")
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
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
|
||||||
@@ -20,6 +19,7 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
34
package.json
34
package.json
@@ -1,15 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "desa-darmasaba",
|
"name": "desa-darmasaba",
|
||||||
"version": "0.1.8",
|
"version": "0.1.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"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"
|
||||||
@@ -34,7 +30,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",
|
||||||
@@ -49,21 +45,17 @@
|
|||||||
"@types/bun": "^1.2.2",
|
"@types/bun": "^1.2.2",
|
||||||
"@types/leaflet": "^1.9.20",
|
"@types/leaflet": "^1.9.20",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/mime-types": "^3.0.1",
|
|
||||||
"@types/nodemailer": "^7.0.2",
|
"@types/nodemailer": "^7.0.2",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"async-mutex": "^0.5.0",
|
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"bun": "^1.2.2",
|
"bun": "^1.2.2",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"cli-progress": "^3.12.0",
|
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"date-fns": "^4.1.0",
|
"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",
|
||||||
@@ -71,7 +63,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.38.0",
|
"framer-motion": "^12.23.5",
|
||||||
"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",
|
||||||
@@ -80,7 +72,6 @@
|
|||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"list": "^2.0.19",
|
"list": "^2.0.19",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mime-types": "^3.0.2",
|
|
||||||
"motion": "^12.4.1",
|
"motion": "^12.4.1",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"next": "^15.5.2",
|
"next": "^15.5.2",
|
||||||
@@ -90,7 +81,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",
|
||||||
@@ -101,7 +92,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": "^3.8.0",
|
"recharts": "^2.15.3",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.3",
|
||||||
"swr": "^2.3.2",
|
"swr": "^2.3.2",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
@@ -111,25 +102,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@playwright/test": "^1.58.2",
|
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
|
||||||
"@types/cli-progress": "^3.11.6",
|
|
||||||
"@types/dompurify": "^3.2.0",
|
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@vitest/ui": "^4.0.18",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.12",
|
"eslint-config-next": "15.1.6",
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
# 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]
|
|
||||||
```
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,25 +0,0 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
testDir: './__tests__/e2e',
|
|
||||||
fullyParallel: true,
|
|
||||||
forbidOnly: !!process.env.CI,
|
|
||||||
retries: process.env.CI ? 2 : 0,
|
|
||||||
workers: process.env.CI ? 1 : undefined,
|
|
||||||
reporter: 'html',
|
|
||||||
use: {
|
|
||||||
baseURL: 'http://localhost:3000',
|
|
||||||
trace: 'on-first-retry',
|
|
||||||
},
|
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: 'chromium',
|
|
||||||
use: { ...devices['Desktop Chrome'] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
webServer: {
|
|
||||||
command: 'bun run dev',
|
|
||||||
url: 'http://localhost:3000',
|
|
||||||
reuseExistingServer: !process.env.CI,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'postcss-preset-mantine': {},
|
'postcss-preset-mantine': {},
|
||||||
'postcss-simple-vars': {
|
'postcss-simple-vars': {
|
||||||
variables: {
|
variables: {
|
||||||
/* Mobile first */
|
'mantine-breakpoint-xs': '36em',
|
||||||
'mantine-breakpoint-xs': '30em', // 480px → mobile kecil–normal
|
'mantine-breakpoint-sm': '48em',
|
||||||
'mantine-breakpoint-sm': '48em', // 768px → tablet / mobile landscape
|
'mantine-breakpoint-md': '62em',
|
||||||
'mantine-breakpoint-md': '64em', // 1024px → laptop & desktop kecil
|
'mantine-breakpoint-lg': '75em',
|
||||||
'mantine-breakpoint-lg': '80em', // 1280px → desktop standar
|
'mantine-breakpoint-xl': '88em',
|
||||||
'mantine-breakpoint-xl': '90em', // 1440px+ → desktop besar
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const appMenuJson = loadJsonData("core/app-menu.json");
|
|
||||||
const appMenuChildJson = loadJsonData("core/app-menu-child.json");
|
|
||||||
|
|
||||||
export async function seedAppMenu() {
|
|
||||||
console.log("🔄 Seeding AppMenu...");
|
|
||||||
|
|
||||||
for (const item of appMenuJson) {
|
|
||||||
await prisma.appMenu.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
name: item.name,
|
|
||||||
link: item.link,
|
|
||||||
isActive: item.isActive,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
link: item.link,
|
|
||||||
isActive: item.isActive,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ AppMenu seeded: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 AppMenu seed selesai");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function seedAppMenuChild() {
|
|
||||||
console.log("🔄 Seeding AppMenuChild...");
|
|
||||||
|
|
||||||
for (const item of appMenuChildJson) {
|
|
||||||
await prisma.appMenuChild.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
name: item.name,
|
|
||||||
link: item.link,
|
|
||||||
isActive: item.isActive,
|
|
||||||
appMenuId: item.appMenuId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
link: item.link,
|
|
||||||
isActive: item.isActive,
|
|
||||||
appMenuId: item.appMenuId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ AppMenuChild seeded: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 AppMenuChild seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const layananJson = loadJsonData("core/layanan.json");
|
|
||||||
const potensiJson = loadJsonData("core/potensi.json");
|
|
||||||
const landingPageLayananJson = loadJsonData("core/landingpage-layanan.json");
|
|
||||||
|
|
||||||
export async function seedLayananCore() {
|
|
||||||
console.log("🔄 Seeding Layanan...");
|
|
||||||
|
|
||||||
for (const item of layananJson) {
|
|
||||||
await prisma.layanan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
name: item.name,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Layanan seeded: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Layanan seed selesai");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function seedPotensiCore() {
|
|
||||||
console.log("🔄 Seeding Potensi...");
|
|
||||||
|
|
||||||
for (const item of potensiJson) {
|
|
||||||
await prisma.potensi.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
name: item.name,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Potensi seeded: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Potensi seed selesai");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function seedLandingPageLayanan() {
|
|
||||||
console.log("🔄 Seeding LandingPage_Layanan...");
|
|
||||||
|
|
||||||
for (const item of landingPageLayananJson) {
|
|
||||||
await prisma.landingPage_Layanan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
deksripsi: item.deksripsi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
deksripsi: item.deksripsi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ LandingPage_Layanan seeded: ${item.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 LandingPage_Layanan seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const kategoriBerita = loadJsonData("desa/berita/kategori-berita.json");
|
|
||||||
const beritaJson = loadJsonData("desa/berita/berita.json");
|
|
||||||
|
|
||||||
export async function seedBerita() {
|
|
||||||
// ================== SUBMENU BERITA ========================
|
|
||||||
console.log("🔄 Seeding Kategori Berita...");
|
|
||||||
for (const k of kategoriBerita) {
|
|
||||||
await prisma.kategoriBerita.upsert({
|
|
||||||
where: {
|
|
||||||
name: k.name, // ✅ cocok dengan @unique
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: k.name,
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id, // ✅ id tetap bisa disimpan
|
|
||||||
name: k.name,
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("kategori berita success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Berita...");
|
|
||||||
|
|
||||||
// Build a map of valid kategori IDs
|
|
||||||
const validKategoriIds = new Set<string>();
|
|
||||||
const kategoriList = await prisma.kategoriBerita.findMany({
|
|
||||||
select: { id: true, name: true },
|
|
||||||
});
|
|
||||||
kategoriList.forEach((k) => validKategoriIds.add(k.id));
|
|
||||||
|
|
||||||
console.log(`📋 Found ${validKategoriIds.size} valid kategori IDs in database`);
|
|
||||||
|
|
||||||
for (const b of beritaJson) {
|
|
||||||
// Validate kategoriBeritaId exists
|
|
||||||
if (!b.kategoriBeritaId || !validKategoriIds.has(b.kategoriBeritaId)) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Skipping berita "${b.judul}": Invalid kategoriBeritaId "${b.kategoriBeritaId}"`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (b.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: b.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for berita "${b.judul}": ${b.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await prisma.berita.upsert({
|
|
||||||
where: { id: b.id },
|
|
||||||
update: {
|
|
||||||
judul: b.judul,
|
|
||||||
deskripsi: b.deskripsi,
|
|
||||||
content: b.content,
|
|
||||||
kategoriBeritaId: b.kategoriBeritaId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: b.id,
|
|
||||||
judul: b.judul,
|
|
||||||
deskripsi: b.deskripsi,
|
|
||||||
content: b.content,
|
|
||||||
kategoriBeritaId: b.kategoriBeritaId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Berita seeded: ${b.judul}`);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(
|
|
||||||
`❌ Failed to seed berita "${b.judul}": ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Berita seed selesai");
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../../load-json";
|
|
||||||
|
|
||||||
const foto = loadJsonData("desa/gallery/foto/foto.json");
|
|
||||||
|
|
||||||
export async function seedFoto() {
|
|
||||||
console.log("🔄 Seeding Foto...");
|
|
||||||
for (const f of foto) {
|
|
||||||
let imagesId: string | null = null;
|
|
||||||
|
|
||||||
if (f.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: f.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for foto "${f.name}": ${f.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imagesId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.galleryFoto.upsert({
|
|
||||||
where: { id: f.id },
|
|
||||||
update: {
|
|
||||||
name: f.name,
|
|
||||||
deskripsi: f.deskripsi,
|
|
||||||
imagesId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: f.id,
|
|
||||||
name: f.name,
|
|
||||||
deskripsi: f.deskripsi,
|
|
||||||
imagesId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Foto seeding completed");
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../../load-json";
|
|
||||||
|
|
||||||
const galleryVideo = loadJsonData("desa/gallery/video/video.json");
|
|
||||||
|
|
||||||
export async function seedVideo() {
|
|
||||||
console.log("🔄 Seeding Gallery Video...");
|
|
||||||
for (const v of galleryVideo) {
|
|
||||||
await prisma.galleryVideo.upsert({
|
|
||||||
where: {
|
|
||||||
id: v.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: v.judul,
|
|
||||||
deskripsi: v.deskripsi,
|
|
||||||
linkVideo: v.linkVideo,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
name: v.judul,
|
|
||||||
deskripsi: v.deskripsi,
|
|
||||||
linkVideo: v.linkVideo,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("gallery video success ...");
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const pelayananSuratKeterangan = loadJsonData("desa/layanan/pelayananSuratKeterangan.json");
|
|
||||||
const pelayananTelunjukSaktiDesa = loadJsonData("desa/layanan/pelayananTelunjukSaktiDesa.json");
|
|
||||||
const pelayananPerizinanBerusaha = loadJsonData("desa/layanan/pelayananPerizinanBerusaha.json");
|
|
||||||
const pelayananPendudukNonPermanen = loadJsonData("desa/layanan/pelayananPendudukNonPermanen.json");
|
|
||||||
|
|
||||||
export async function seedLayanan() {
|
|
||||||
console.log("🔄 Seeding Pelayanan Surat Keterangan...");
|
|
||||||
|
|
||||||
for (const p of pelayananSuratKeterangan) {
|
|
||||||
const existing = await prisma.pelayananSuratKeterangan.findUnique({
|
|
||||||
where: { id: p.id },
|
|
||||||
select: { imageId: true, image2Id: true }, // 📌 tambahkan image2Id
|
|
||||||
});
|
|
||||||
|
|
||||||
// 1️⃣ Handle imageId
|
|
||||||
let imageId = existing?.imageId ?? null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for pelayanan surat keterangan 1 "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2️⃣ Handle image2Id
|
|
||||||
let image2Id = existing?.image2Id ?? null;
|
|
||||||
|
|
||||||
if (p.image2Name) {
|
|
||||||
const image2 = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.image2Name },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image2) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for pelayanan surat keterangan 2 "${p.name}": ${p.image2Name}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
image2Id = image2.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3️⃣ Upsert dengan kedua image
|
|
||||||
await prisma.pelayananSuratKeterangan.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
image2Id, // 📌 tambahkan ini
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
image2Id, // 📌 tambahkan ini
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Pelayanan Surat Keterangan success...");
|
|
||||||
|
|
||||||
for (const p of pelayananTelunjukSaktiDesa) {
|
|
||||||
await prisma.pelayananTelunjukSaktiDesa.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
link: p.link,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
link: p.link,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("pelayanan telunjuk sakti desa success ...");
|
|
||||||
|
|
||||||
for (const l of pelayananPerizinanBerusaha) {
|
|
||||||
await prisma.pelayananPerizinanBerusaha.upsert({
|
|
||||||
where: {
|
|
||||||
id: l.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: l.name,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
link: l.link,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: l.id,
|
|
||||||
name: l.name,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
link: l.link,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("pelayanan perizinan berusaha success ...");
|
|
||||||
|
|
||||||
for (const l of pelayananPendudukNonPermanen) {
|
|
||||||
await prisma.pelayananPendudukNonPermanen.upsert({
|
|
||||||
where: {
|
|
||||||
id: l.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: l.name,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: l.id,
|
|
||||||
name: l.name,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("pelayanan penduduk non permanen success ...");
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const musikJson = loadJsonData("desa/musik-desa/musik-desa.json");
|
|
||||||
|
|
||||||
export async function seedMusikDesa() {
|
|
||||||
console.log("Seeding Musik Desa...");
|
|
||||||
|
|
||||||
for (const item of musikJson) {
|
|
||||||
let audioFileId: string | null = null;
|
|
||||||
let coverImageId: string | null = null;
|
|
||||||
|
|
||||||
if (item.audioFileName) {
|
|
||||||
const audio = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: item.audioFileName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (audio) audioFileId = audio.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.coverImageName) {
|
|
||||||
const cover = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: item.coverImageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (cover) coverImageId = cover.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.musikDesa.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
judul: item.judul,
|
|
||||||
artis: item.artis,
|
|
||||||
deskripsi: item.deskripsi,
|
|
||||||
durasi: item.durasi,
|
|
||||||
audioFileId,
|
|
||||||
coverImageId,
|
|
||||||
genre: item.genre,
|
|
||||||
tahunRilis: item.tahunRilis,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
judul: item.judul,
|
|
||||||
artis: item.artis,
|
|
||||||
deskripsi: item.deskripsi,
|
|
||||||
durasi: item.durasi,
|
|
||||||
audioFileId,
|
|
||||||
coverImageId,
|
|
||||||
genre: item.genre,
|
|
||||||
tahunRilis: item.tahunRilis,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Musik: ${item.judul} - ${item.artis}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Musik Desa seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const penghargaan = loadJsonData("desa/penghargaan/penghargaan.json");
|
|
||||||
|
|
||||||
export async function seedPenghargaan() {
|
|
||||||
console.log("🔄 Seeding Penghargaan...");
|
|
||||||
for (const m of penghargaan) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (m.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: m.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for penghargaan "${m.name}": ${m.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.penghargaan.upsert({
|
|
||||||
where: { id: m.id },
|
|
||||||
update: {
|
|
||||||
name: m.name,
|
|
||||||
juara: m.juara,
|
|
||||||
deskripsi: m.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: m.id,
|
|
||||||
name: m.name,
|
|
||||||
juara: m.juara,
|
|
||||||
deskripsi: m.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("penghargaan success ...");
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
import { safeSeedUnique } from "../../../safeseedUnique";
|
|
||||||
|
|
||||||
const kategoriPengumuman = loadJsonData("desa/pengumuman/kategori-pengumuman.json");
|
|
||||||
const pengumuman = loadJsonData("desa/pengumuman/pengumuman.json");
|
|
||||||
|
|
||||||
export async function seedPengumuman() {
|
|
||||||
console.log("🔄 Seeding Kategori Pengumuman...");
|
|
||||||
for (const c of kategoriPengumuman) {
|
|
||||||
await safeSeedUnique(
|
|
||||||
"categoryPengumuman",
|
|
||||||
{ name: c.name }, // ✅ where clause
|
|
||||||
{
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("kategori pengumuman success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Pengumuman...");
|
|
||||||
for (const p of pengumuman) {
|
|
||||||
await prisma.pengumuman.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
judul: p.judul,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
content: p.content,
|
|
||||||
categoryPengumumanId: p.categoryPengumumanId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
judul: p.judul,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
content: p.content,
|
|
||||||
categoryPengumumanId: p.categoryPengumumanId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("pengumuman success ...");
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const kategoriPotensi = loadJsonData("desa/potensi/kategori-potensi.json");
|
|
||||||
const potensiDesa = loadJsonData("desa/potensi/potensi-desa.json");
|
|
||||||
|
|
||||||
export async function seedPotensi() {
|
|
||||||
console.log("🔄Seeding Kategori Potensi Desa ...");
|
|
||||||
for (const c of kategoriPotensi) {
|
|
||||||
await prisma.kategoriPotensi.upsert({
|
|
||||||
where: {
|
|
||||||
id: c.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
nama: c.nama,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: c.id,
|
|
||||||
nama: c.nama,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("kategori Potensi success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Potensi Desa...");
|
|
||||||
for (const m of potensiDesa) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (m.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: m.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for potensi desa "${m.name}": ${m.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.potensiDesa.upsert({
|
|
||||||
where: { id: m.id },
|
|
||||||
update: {
|
|
||||||
name: m.name,
|
|
||||||
deskripsi: m.deskripsi,
|
|
||||||
content: m.content,
|
|
||||||
kategoriId: m.kategoriId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: m.id,
|
|
||||||
name: m.name,
|
|
||||||
deskripsi: m.deskripsi,
|
|
||||||
content: m.content,
|
|
||||||
kategoriId: m.kategoriId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("potensi desa success ...");
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const lambangDesa = loadJsonData("desa/profile/lambang_desa.json");
|
|
||||||
const maskotDesa = loadJsonData("desa/profile/maskot_desa.json");
|
|
||||||
const profilePerbekel = loadJsonData("desa/profile/profil_perbekel.json");
|
|
||||||
const profileDesaImage = loadJsonData("desa/profile/profileDesaImage.json");
|
|
||||||
const sejarahDesa = loadJsonData("desa/profile/sejarah_desa.json");
|
|
||||||
const visiMisiDesa = loadJsonData("desa/profile/visi_misi_desa.json");
|
|
||||||
|
|
||||||
export async function seedProfileDesa() {
|
|
||||||
// =========== SEJARAH DESA ===========
|
|
||||||
for (const l of sejarahDesa) {
|
|
||||||
await prisma.sejarahDesa.upsert({
|
|
||||||
where: {
|
|
||||||
id: l.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
judul: l.judul,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: l.id,
|
|
||||||
judul: l.judul,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("sejarah desa success ...");
|
|
||||||
|
|
||||||
// =========== VISI MISI DESA ===========
|
|
||||||
for (const l of visiMisiDesa) {
|
|
||||||
await prisma.visiMisiDesa.upsert({
|
|
||||||
where: {
|
|
||||||
id: l.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
visi: l.visi,
|
|
||||||
misi: l.misi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: l.id,
|
|
||||||
visi: l.visi,
|
|
||||||
misi: l.misi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("visi misi desa success ...");
|
|
||||||
|
|
||||||
// =========== MASKOT DESA ===========
|
|
||||||
for (const l of maskotDesa) {
|
|
||||||
await prisma.maskotDesa.upsert({
|
|
||||||
where: {
|
|
||||||
id: l.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
judul: l.judul,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: l.id,
|
|
||||||
judul: l.judul,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("maskot desa success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Profile Desa Image...");
|
|
||||||
for (const m of profileDesaImage) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (m.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: m.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for profile desa image "${m.label}": ${m.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.profileDesaImage.upsert({
|
|
||||||
where: { id: m.id },
|
|
||||||
update: {
|
|
||||||
label: m.label,
|
|
||||||
maskotDesaId: m.maskotDesaId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: m.id,
|
|
||||||
label: m.label,
|
|
||||||
maskotDesaId: m.maskotDesaId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("profile desa image success ...");
|
|
||||||
|
|
||||||
// =========== LAMBANG DESA ===========
|
|
||||||
for (const l of lambangDesa) {
|
|
||||||
await prisma.lambangDesa.upsert({
|
|
||||||
where: {
|
|
||||||
id: l.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
judul: l.judul,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: l.id,
|
|
||||||
judul: l.judul,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("lambang desa success ...");
|
|
||||||
|
|
||||||
// =========== PROFILE PERBEKEL PROFILE DESA ===========
|
|
||||||
console.log("🔄 Seeding Profile Perbekel...");
|
|
||||||
for (const m of profilePerbekel) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (m.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: m.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for profile perbekel "${m.biodata}": ${m.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.profilPerbekel.upsert({
|
|
||||||
where: { id: m.id },
|
|
||||||
update: {
|
|
||||||
biodata: m.biodata,
|
|
||||||
pengalaman: m.pengalaman,
|
|
||||||
pengalamanOrganisasi: m.pengalamanOrganisasi,
|
|
||||||
programUnggulan: m.programUnggulan,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: m.id,
|
|
||||||
biodata: m.biodata,
|
|
||||||
pengalaman: m.pengalaman,
|
|
||||||
pengalamanOrganisasi: m.pengalamanOrganisasi,
|
|
||||||
programUnggulan: m.programUnggulan,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("profile perbekel desa success ...");
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const perbekelDariMasaKeMasa = loadJsonData("desa/profile/profile-perbekel-lalu.json");
|
|
||||||
|
|
||||||
export async function seedProfilePerbekel() {
|
|
||||||
console.log("🔄 Seeding Perbekel Dari Masa Ke Masa...");
|
|
||||||
for (const p of perbekelDariMasaKeMasa) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for Perbekel Dari Masa Ke Masa "${p.nama}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.perbekelDariMasaKeMasa.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
nama: p.nama,
|
|
||||||
periode: p.periode,
|
|
||||||
daerah: p.daerah,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
nama: p.nama,
|
|
||||||
periode: p.periode,
|
|
||||||
daerah: p.daerah,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Pejabat Desa seeding completed");
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const apbdesJson = loadJsonData("ekonomi/apbdes/apbdes.json");
|
|
||||||
|
|
||||||
export async function seedAPBDes() {
|
|
||||||
console.log("Seeding APBDes...");
|
|
||||||
|
|
||||||
for (const item of apbdesJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
let fileId: string | null = null;
|
|
||||||
|
|
||||||
if (item.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: item.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
if (image) imageId = image.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.aPBDes.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
tahun: item.tahun,
|
|
||||||
name: item.name,
|
|
||||||
deskripsi: item.deskripsi,
|
|
||||||
jumlah: item.jumlah,
|
|
||||||
imageId,
|
|
||||||
fileId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
tahun: item.tahun,
|
|
||||||
name: item.name,
|
|
||||||
deskripsi: item.deskripsi,
|
|
||||||
jumlah: item.jumlah,
|
|
||||||
imageId,
|
|
||||||
fileId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` APBDes: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("APBDes seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const itemsJson = loadJsonData("ekonomi/apbdes/apbdes-items.json");
|
|
||||||
const realisasiJson = loadJsonData("ekonomi/apbdes/realisasi-items.json");
|
|
||||||
|
|
||||||
export async function seedAPBDesItem() {
|
|
||||||
console.log("Seeding APBDes Items...");
|
|
||||||
|
|
||||||
// Seed items first (sorted by level to ensure parents exist)
|
|
||||||
const sortedItems = [...itemsJson].sort((a, b) => a.level - b.level);
|
|
||||||
|
|
||||||
for (const item of sortedItems) {
|
|
||||||
await prisma.aPBDesItem.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
kode: item.kode,
|
|
||||||
uraian: item.uraian,
|
|
||||||
anggaran: item.anggaran,
|
|
||||||
tipe: item.tipe,
|
|
||||||
level: item.level,
|
|
||||||
parentId: item.parentId,
|
|
||||||
apbdesId: item.apbdesId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
kode: item.kode,
|
|
||||||
uraian: item.uraian,
|
|
||||||
anggaran: item.anggaran,
|
|
||||||
tipe: item.tipe,
|
|
||||||
level: item.level,
|
|
||||||
parentId: item.parentId,
|
|
||||||
apbdesId: item.apbdesId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` APBDes Item: ${item.kode} - ${item.uraian}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Realisasi Items...");
|
|
||||||
for (const item of realisasiJson) {
|
|
||||||
await prisma.realisasiItem.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
kode: item.kode,
|
|
||||||
apbdesItemId: item.apbdesItemId,
|
|
||||||
jumlah: item.jumlah,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
keterangan: item.keterangan,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
kode: item.kode,
|
|
||||||
apbdesItemId: item.apbdesItemId,
|
|
||||||
jumlah: item.jumlah,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
keterangan: item.keterangan,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Realisasi: ${item.kode} - Rp ${item.jumlah.toLocaleString("id-ID")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("APBDes Item & Realisasi seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const demografiPekerjaan = loadJsonData("ekonomi/demografi-pekerjaan/demografi-pekerjaan.json");
|
|
||||||
|
|
||||||
export async function seedDemografiPekerjaan() {
|
|
||||||
console.log("🔄 Seeding Demografi Pekerjaan...");
|
|
||||||
for (const k of demografiPekerjaan) {
|
|
||||||
await prisma.dataDemografiPekerjaan.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
pekerjaan: k.pekerjaan,
|
|
||||||
lakiLaki: k.lakiLaki,
|
|
||||||
perempuan: k.perempuan,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
pekerjaan: k.pekerjaan,
|
|
||||||
lakiLaki: k.lakiLaki,
|
|
||||||
perempuan: k.perempuan,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Demografi Pekerjaan seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const jumlahPendudukMiskin = loadJsonData("ekonomi/jumlah-penduduk-miskin/jumlah-penduduk-miskin.json");
|
|
||||||
|
|
||||||
export async function seedJumlahPendudukMiskin() {
|
|
||||||
console.log("🔄 Seeding Jumlah Penduduk Miskin...");
|
|
||||||
for (const k of jumlahPendudukMiskin) {
|
|
||||||
await prisma.grafikJumlahPendudukMiskin.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
year: k.year,
|
|
||||||
totalPoorPopulation: k.totalPoorPopulation,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
year: k.year,
|
|
||||||
totalPoorPopulation: k.totalPoorPopulation,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Jumlah Penduduk Miskin seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const jumlahPengangguran = loadJsonData("ekonomi/jumlah-pengangguran/detail-data-pengangguran.json");
|
|
||||||
|
|
||||||
export async function seedJumlahPengangguran() {
|
|
||||||
for (const d of jumlahPengangguran) {
|
|
||||||
await prisma.detailDataPengangguran.upsert({
|
|
||||||
where: {
|
|
||||||
month_year: { month: d.month, year: d.year },
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
totalUnemployment: d.totalUnemployment,
|
|
||||||
educatedUnemployment: d.educatedUnemployment,
|
|
||||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
|
||||||
percentageChange: d.percentageChange,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
month: d.month,
|
|
||||||
year: d.year,
|
|
||||||
totalUnemployment: d.totalUnemployment,
|
|
||||||
educatedUnemployment: d.educatedUnemployment,
|
|
||||||
uneducatedUnemployment: d.uneducatedUnemployment,
|
|
||||||
percentageChange: d.percentageChange,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("📊 detailDataPengangguran success ...");
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const lowonganKerjaLokal = loadJsonData("ekonomi/lowongan-kerja-lokal/lowongan-kerja-lokal.json");
|
|
||||||
|
|
||||||
export async function seedLowonganKerjaLokal() {
|
|
||||||
console.log("🔄 Seeding Lowongan Kerja Lokal...");
|
|
||||||
for (const k of lowonganKerjaLokal) {
|
|
||||||
await prisma.lowonganPekerjaan.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
posisi: k.posisi,
|
|
||||||
namaPerusahaan: k.namaPerusahaan,
|
|
||||||
lokasi: k.lokasi,
|
|
||||||
tipePekerjaan: k.tipePekerjaan,
|
|
||||||
gaji: k.gaji,
|
|
||||||
deskripsi: k.deskripsi,
|
|
||||||
kualifikasi: k.kualifikasi,
|
|
||||||
notelp: k.notelp,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
posisi: k.posisi,
|
|
||||||
namaPerusahaan: k.namaPerusahaan,
|
|
||||||
lokasi: k.lokasi,
|
|
||||||
tipePekerjaan: k.tipePekerjaan,
|
|
||||||
gaji: k.gaji,
|
|
||||||
deskripsi: k.deskripsi,
|
|
||||||
kualifikasi: k.kualifikasi,
|
|
||||||
notelp: k.notelp,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Lowongan Kerja Lokal seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const kategoriProduk = loadJsonData("ekonomi/pasar-desa/kategori-produk.json");
|
|
||||||
const pasarDesa = loadJsonData("ekonomi/pasar-desa/pasar-desa.json");
|
|
||||||
const kategoriToPasar = loadJsonData("ekonomi/pasar-desa/kategori-to-pasar.json");
|
|
||||||
|
|
||||||
export async function seedPasarDesa() {
|
|
||||||
console.log("🔄 Seeding Kategori Produk...");
|
|
||||||
for (const k of kategoriProduk) {
|
|
||||||
await prisma.kategoriProduk.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
nama: k.nama,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
nama: k.nama,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Kategori Produk seeded successfully");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Pasar Desa...");
|
|
||||||
|
|
||||||
for (const p of pasarDesa) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for pasar desa "${p.nama}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.pasarDesa.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
nama: p.nama,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
harga: p.harga,
|
|
||||||
rating: p.rating,
|
|
||||||
alamatUsaha: p.alamatUsaha,
|
|
||||||
kontak: p.kontak,
|
|
||||||
imageId,
|
|
||||||
kategoriProdukId: p.kategoriProdukId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
nama: p.nama,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
harga: p.harga,
|
|
||||||
rating: p.rating,
|
|
||||||
alamatUsaha: p.alamatUsaha,
|
|
||||||
kontak: p.kontak,
|
|
||||||
imageId,
|
|
||||||
kategoriProdukId: p.kategoriProdukId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Pasar desa seeded: ${p.nama}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Pasar desa seed selesai");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Kategori To Pasar...");
|
|
||||||
for (const p of kategoriToPasar) {
|
|
||||||
await prisma.kategoriToPasar.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
kategoriId: p.kategoriId,
|
|
||||||
pasarDesaId: p.pasarDesaId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
kategoriId: p.kategoriId,
|
|
||||||
pasarDesaId: p.pasarDesaId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const apbdes = loadJsonData("ekonomi/pendapatan-asli-desa/apbDesa.json");
|
|
||||||
const pendapatan = loadJsonData("ekonomi/pendapatan-asli-desa/pendapatanDesa.json");
|
|
||||||
const belanja = loadJsonData("ekonomi/pendapatan-asli-desa/belanjaDesa.json");
|
|
||||||
const pembiayaan = loadJsonData("ekonomi/pendapatan-asli-desa/pembiayaanDesa.json");
|
|
||||||
|
|
||||||
export async function seedPendapatanAsli() {
|
|
||||||
console.log("🔄 Seeding Pendapatan Asli...");
|
|
||||||
for (const d of apbdes) {
|
|
||||||
await prisma.apbDesa.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
tahun: d.tahun,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
tahun: d.tahun,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Pendapatan Asli seeded successfully");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Pendapatan...");
|
|
||||||
for (const d of pendapatan) {
|
|
||||||
await prisma.pendapatan.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: d.name,
|
|
||||||
value: d.nilai
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
name: d.name,
|
|
||||||
value: d.nilai
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Pendapatan seeded successfully");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Belanja...");
|
|
||||||
for (const d of belanja) {
|
|
||||||
await prisma.belanja.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: d.name,
|
|
||||||
value: d.nilai
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
name: d.name,
|
|
||||||
value: d.nilai
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Belanja seeded successfully");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Pembiayaan...");
|
|
||||||
for (const d of pembiayaan) {
|
|
||||||
await prisma.pembiayaan.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: d.name,
|
|
||||||
value: d.nilai
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
name: d.name,
|
|
||||||
value: d.nilai
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Pembiayaan seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const grafikMenganggurBerdasarkanUsia = loadJsonData("ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-usia.json");
|
|
||||||
const grafikMenganggurBerdasarkanPendidikan = loadJsonData("ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran-berdasarkan-pendidikan.json");
|
|
||||||
|
|
||||||
export async function seedPendudukUsiaKerjaYangMenganggur() {
|
|
||||||
for (const p of grafikMenganggurBerdasarkanUsia) {
|
|
||||||
await prisma.grafikMenganggurBerdasarkanUsia.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
usia18_25: p.usia18_25,
|
|
||||||
usia26_35: p.usia26_35,
|
|
||||||
usia36_45: p.usia36_45,
|
|
||||||
usia46_keatas: p.usia46_keatas,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
usia18_25: p.usia18_25,
|
|
||||||
usia26_35: p.usia26_35,
|
|
||||||
usia36_45: p.usia36_45,
|
|
||||||
usia46_keatas: p.usia46_keatas,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
|
||||||
for (const p of grafikMenganggurBerdasarkanPendidikan) {
|
|
||||||
await prisma.grafikMenganggurBerdasarkanPendidikan.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
SD: p.SD,
|
|
||||||
SMP: p.SMP,
|
|
||||||
SMA: p.SMA,
|
|
||||||
D3: p.D3,
|
|
||||||
S1: p.S1,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
SD: p.SD,
|
|
||||||
SMP: p.SMP,
|
|
||||||
SMA: p.SMA,
|
|
||||||
D3: p.D3,
|
|
||||||
S1: p.S1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("📊 grafikMenganggurBerdasarkanUsia success ...");
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const programKemiskinan = loadJsonData("ekonomi/program-kemiskinan/program-kemiskinan.json");
|
|
||||||
const statistikKemiskinan = loadJsonData("ekonomi/program-kemiskinan/statistik-kemiskinan.json");
|
|
||||||
|
|
||||||
export async function seedProgramKemiskinan() {
|
|
||||||
for (const s of statistikKemiskinan) {
|
|
||||||
await prisma.statistikKemiskinan.upsert({
|
|
||||||
where: { tahun: s.tahun }, // ✅ FIX
|
|
||||||
update: {
|
|
||||||
jumlah: s.jumlah,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: s.id, // id boleh tetap
|
|
||||||
tahun: s.tahun,
|
|
||||||
jumlah: s.jumlah,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("📊 Statistik Kemiskinan seeded successfully");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Program Kemiskinan...");
|
|
||||||
for (const k of programKemiskinan) {
|
|
||||||
await prisma.programKemiskinan.upsert({
|
|
||||||
where: { id: k.id },
|
|
||||||
update: {
|
|
||||||
nama: k.nama,
|
|
||||||
deskripsi: k.deskripsi,
|
|
||||||
icon: k.icon,
|
|
||||||
statistik: {
|
|
||||||
connect: {
|
|
||||||
tahun: k.tahun, // 👈 BUKAN ID
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
nama: k.nama,
|
|
||||||
deskripsi: k.deskripsi,
|
|
||||||
icon: k.icon,
|
|
||||||
statistik: {
|
|
||||||
connect: {
|
|
||||||
tahun: k.tahun,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Program Kemiskinan seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const sektorUnggulanDesa = loadJsonData("ekonomi/sektor-unggulan/sektor-unggulan.json");
|
|
||||||
|
|
||||||
export async function seedSektorUnggulanDesa() {
|
|
||||||
console.log("🔄 Seeding Sektor Unggulan Desa...");
|
|
||||||
for (const k of sektorUnggulanDesa) {
|
|
||||||
await prisma.sektorUnggulanDesa.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: k.name,
|
|
||||||
description: k.description,
|
|
||||||
value: k.value,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
name: k.name,
|
|
||||||
description: k.description,
|
|
||||||
value: k.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Sektor Unggulan Desa seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
interface PosisiOrganisasi {
|
|
||||||
id: string;
|
|
||||||
nama: string;
|
|
||||||
deskripsi: string;
|
|
||||||
hierarki: number;
|
|
||||||
parentId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PegawaiBumDes {
|
|
||||||
id: string;
|
|
||||||
namaLengkap: string;
|
|
||||||
gelarAkademik: string;
|
|
||||||
tanggalMasuk: string;
|
|
||||||
email: string;
|
|
||||||
telepon: string;
|
|
||||||
alamat: string;
|
|
||||||
posisiId: string;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const posisiOrganisasiBumDes = loadJsonData<PosisiOrganisasi[][]>("ekonomi/struktur-organisasi/posisi-organisasi-bumdes.json");
|
|
||||||
const pegawai = loadJsonData<PegawaiBumDes[]>("ekonomi/struktur-organisasi/pegawai-bumdes.json");
|
|
||||||
|
|
||||||
export async function seedStrukturBumdes() {
|
|
||||||
const flattenedPosisi = posisiOrganisasiBumDes.flat();
|
|
||||||
|
|
||||||
// ✅ Urutkan berdasarkan hierarki
|
|
||||||
const sortedPosisi = flattenedPosisi.sort((a, b) => a.hierarki - b.hierarki);
|
|
||||||
|
|
||||||
for (const p of sortedPosisi) {
|
|
||||||
console.log(`Seeding: ${p.nama} (id: ${p.id}, parent: ${p.parentId})`);
|
|
||||||
if (p.parentId) {
|
|
||||||
const parentExists = flattenedPosisi.some((pos) => pos.id === p.parentId);
|
|
||||||
if (!parentExists) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Parent tidak ditemukan: ${p.parentId} untuk ${p.nama}`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await prisma.posisiOrganisasiBumDes.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: p,
|
|
||||||
create: p,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("posisi organisasi berhasil");
|
|
||||||
for (const p of pegawai) {
|
|
||||||
await prisma.pegawaiBumDes.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
namaLengkap: p.namaLengkap,
|
|
||||||
gelarAkademik: p.gelarAkademik,
|
|
||||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
|
||||||
email: p.email,
|
|
||||||
telepon: p.telepon,
|
|
||||||
alamat: p.alamat,
|
|
||||||
posisiId: p.posisiId,
|
|
||||||
isActive: p.isActive,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
namaLengkap: p.namaLengkap,
|
|
||||||
gelarAkademik: p.gelarAkademik,
|
|
||||||
tanggalMasuk: new Date(p.tanggalMasuk),
|
|
||||||
email: p.email,
|
|
||||||
telepon: p.telepon,
|
|
||||||
alamat: p.alamat,
|
|
||||||
posisiId: p.posisiId,
|
|
||||||
isActive: p.isActive,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("pegawai success ...");
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const ajukanIde = loadJsonData("inovasi/ajukan-ide/ajukan-ide.json");
|
|
||||||
|
|
||||||
export async function seedAjukan() {
|
|
||||||
console.log("🔄 Seeding Ajukan Ide Inovatif...");
|
|
||||||
for (const d of ajukanIde) {
|
|
||||||
await prisma.ajukanIdeInovatif.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: d.name,
|
|
||||||
alamat: d.alamat,
|
|
||||||
namaIde: d.namaIde,
|
|
||||||
deskripsi: d.deskripsi,
|
|
||||||
masalah: d.masalah,
|
|
||||||
benefit: d.benefit,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
name: d.name,
|
|
||||||
alamat: d.alamat,
|
|
||||||
namaIde: d.namaIde,
|
|
||||||
deskripsi: d.deskripsi,
|
|
||||||
masalah: d.masalah,
|
|
||||||
benefit: d.benefit,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Ajukan Ide Inovatif seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const desaDigital = loadJsonData("inovasi/desa-digital/desa-digital.json");
|
|
||||||
|
|
||||||
export async function seedDesaDigital() {
|
|
||||||
console.log("🔄 Seeding Desa Digital...");
|
|
||||||
for (const d of desaDigital) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (d.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: d.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for desa digital "${d.name}": ${d.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.desaDigital.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: d.name,
|
|
||||||
deskripsi: d.deskripsi,
|
|
||||||
imageId: imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
name: d.name,
|
|
||||||
deskripsi: d.deskripsi,
|
|
||||||
imageId: imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Desa Digital seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const infoTeknologi = loadJsonData("inovasi/info-teknologi/info-teknologi.json");
|
|
||||||
|
|
||||||
export async function seedInfoTeknologi() {
|
|
||||||
console.log("🔄 Seeding Info Teknologi...");
|
|
||||||
for (const p of infoTeknologi) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for berita "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.infoTekno.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId: imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId: imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Info Teknologi seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const kolaborasiInovasi = loadJsonData("inovasi/kolaborasi-inovasi/kolaborasi-inovasi.json");
|
|
||||||
const mitraKolaborasi = loadJsonData("inovasi/kolaborasi-inovasi/mitra-kolaborasi.json");
|
|
||||||
|
|
||||||
export async function seedKolaborasiInovasi() {
|
|
||||||
console.log("🔄 Seeding Kolaborasi Inovasi...");
|
|
||||||
for (const p of kolaborasiInovasi) {
|
|
||||||
await prisma.kolaborasiInovasi.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
tahun: p.tahun,
|
|
||||||
slug: p.slug,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
kolaborator: p.kolaborator,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
tahun: p.tahun,
|
|
||||||
slug: p.slug,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
kolaborator: p.kolaborator,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Kolaborasi Inovasi seeded successfully");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Mitra Kolaborasi...");
|
|
||||||
for (const p of mitraKolaborasi) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for mitra kolaborasi "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.mitraKolaborasi.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
imageId: imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
imageId: imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Mitra Kolaborasi seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const jenisLayanan = loadJsonData("inovasi/layanan-online-desa/jenis-layanan.json");
|
|
||||||
const administrasiOnline = loadJsonData("inovasi/layanan-online-desa/administrasi-online.json");
|
|
||||||
const jenisPengaduan = loadJsonData("inovasi/layanan-online-desa/jenis-pengaduan.json");
|
|
||||||
const pengaduanMasyarakat = loadJsonData("inovasi/layanan-online-desa/pengaduan-masyarakat.json");
|
|
||||||
|
|
||||||
export async function seedLayananOnlineDesa() {
|
|
||||||
console.log("🔄 Seeding Jenis Layanan...");
|
|
||||||
for (const j of jenisLayanan) {
|
|
||||||
await prisma.jenisLayanan.upsert({
|
|
||||||
where: {
|
|
||||||
id: j.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
nama: j.nama,
|
|
||||||
deskripsi: j.deskripsi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: j.id,
|
|
||||||
nama: j.nama,
|
|
||||||
deskripsi: j.deskripsi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Jenis Layanan seeded successfully");
|
|
||||||
console.log("🔄 Seeding Administrasi Online...");
|
|
||||||
for (const d of administrasiOnline) {
|
|
||||||
await prisma.administrasiOnline.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: d.name,
|
|
||||||
alamat: d.alamat,
|
|
||||||
nomorTelepon: d.nomorTelepon,
|
|
||||||
jenisLayananId: d.jenisLayananId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
name: d.name,
|
|
||||||
alamat: d.alamat,
|
|
||||||
nomorTelepon: d.nomorTelepon,
|
|
||||||
jenisLayananId: d.jenisLayananId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Administrasi Online seeded successfully");
|
|
||||||
console.log("🔄 Seeding Jenis Pengaduan Masyarakat...");
|
|
||||||
for (const d of jenisPengaduan) {
|
|
||||||
await prisma.jenisPengaduan.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
nama: d.nama,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
nama: d.nama,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Jenis Pengaduan Masyarakat seeded successfully");
|
|
||||||
console.log("🔄 Seeding Pengaduan Masyarakat...");
|
|
||||||
for (const d of pengaduanMasyarakat) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (d.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: d.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for pengaduan masyarakat "${d.name}": ${d.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.pengaduanMasyarakat.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: d.name,
|
|
||||||
email: d.email,
|
|
||||||
nik: d.nik,
|
|
||||||
nomorTelepon: d.nomorTelepon,
|
|
||||||
judulPengaduan: d.judulPengaduan,
|
|
||||||
lokasiKejadian: d.lokasiKejadian,
|
|
||||||
imageId: imageId,
|
|
||||||
deskripsiPengaduan: d.deskripsiPengaduan,
|
|
||||||
jenisPengaduanId: d.jenisPengaduanId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
name: d.name,
|
|
||||||
email: d.email,
|
|
||||||
nik: d.nik,
|
|
||||||
nomorTelepon: d.nomorTelepon,
|
|
||||||
judulPengaduan: d.judulPengaduan,
|
|
||||||
lokasiKejadian: d.lokasiKejadian,
|
|
||||||
imageId: imageId,
|
|
||||||
deskripsiPengaduan: d.deskripsiPengaduan,
|
|
||||||
jenisPengaduanId: d.jenisPengaduanId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Pengaduan Masyarakat seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const programKreatif = loadJsonData("inovasi/program-kreatif-desa/program-kreatif-desa.json");
|
|
||||||
|
|
||||||
export async function seedProgramKreatifDesa() {
|
|
||||||
console.log("🔄 Seeding Program Kreatif...");
|
|
||||||
for (const p of programKreatif) {
|
|
||||||
await prisma.programKreatif.upsert({
|
|
||||||
where: {
|
|
||||||
id: p.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
icon: p.icon,
|
|
||||||
slug: p.slug,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
icon: p.icon,
|
|
||||||
slug: p.slug,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Program Kreatif seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const keamananLingkunganJson = loadJsonData("keamanan/keamanan-lingkungan/keamanan-lingkungan.json");
|
|
||||||
|
|
||||||
export async function seedKeamananLingkungan() {
|
|
||||||
console.log("🔄 Seeding Keamanan Lingkungan...");
|
|
||||||
|
|
||||||
for (const p of keamananLingkunganJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for keamanan lingkungan "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.keamananLingkungan.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Keamanan lingkungan seeded: ${p.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Keamanan lingkungan seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const kontakDaruratKeamanan = loadJsonData("keamanan/kontak-darurat-keamanan/kontak-darurat-keamanan.json");
|
|
||||||
const kontakItem = loadJsonData("keamanan/kontak-darurat-keamanan/kontakItem.json");
|
|
||||||
const kontakDaruratToItem = loadJsonData("keamanan/kontak-darurat-keamanan/kontakDaruratToItem.json");
|
|
||||||
|
|
||||||
export async function seedKontakDaruratKeamanan() {
|
|
||||||
console.log("🔄 Seeding Kontak Item...");
|
|
||||||
for (const e of kontakItem) {
|
|
||||||
await prisma.kontakItem.upsert({
|
|
||||||
where: {
|
|
||||||
id: e.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
nama: e.nama,
|
|
||||||
icon: e.icon,
|
|
||||||
nomorTelepon: e.nomorTelepon,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: e.id, // ✅ WAJIB
|
|
||||||
nama: e.nama,
|
|
||||||
icon: e.icon,
|
|
||||||
nomorTelepon: e.nomorTelepon,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Kontak Item seeded successfully");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Kontak Darurat Keamanan...");
|
|
||||||
for (const d of kontakDaruratKeamanan) {
|
|
||||||
await prisma.kontakDaruratKeamanan.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
nama: d.nama,
|
|
||||||
icon: d.icon,
|
|
||||||
kategoriId: d.kategoriId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
nama: d.nama,
|
|
||||||
icon: d.icon,
|
|
||||||
kategoriId: d.kategoriId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Kontak Darurat Keamanan seeded successfully");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Kontak Darurat To Item...");
|
|
||||||
for (const f of kontakDaruratToItem) {
|
|
||||||
// ✅ Validasi foreign keys
|
|
||||||
const kontakDaruratExists = await prisma.kontakDaruratKeamanan.findUnique({
|
|
||||||
where: { id: f.kontakDaruratId },
|
|
||||||
});
|
|
||||||
|
|
||||||
const kontakItemExists = await prisma.kontakItem.findUnique({
|
|
||||||
where: { id: f.kontakItemId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!kontakDaruratExists) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ KontakDarurat ${f.kontakDaruratId} not found, skipping...`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!kontakItemExists) {
|
|
||||||
console.warn(`⚠️ KontakItem ${f.kontakItemId} not found, skipping...`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.kontakDaruratToItem.upsert({
|
|
||||||
where: { id: f.id },
|
|
||||||
update: {
|
|
||||||
kontakDaruratId: f.kontakDaruratId,
|
|
||||||
kontakItemId: f.kontakItemId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: f.id,
|
|
||||||
kontakDaruratId: f.kontakDaruratId,
|
|
||||||
kontakItemId: f.kontakItemId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Kontak Darurat To Item seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const laporanPublik = loadJsonData("keamanan/laporan-publik/laporan-publik.json");
|
|
||||||
const penangananLaporan = loadJsonData("keamanan/laporan-publik/penanganan-laporan.json");
|
|
||||||
|
|
||||||
export async function seedLaporanPublik() {
|
|
||||||
console.log("🔄 Seeding Laporan Publik...");
|
|
||||||
for (const l of laporanPublik) {
|
|
||||||
await prisma.laporanPublik.upsert({
|
|
||||||
where: {
|
|
||||||
id: l.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
judul: l.judul,
|
|
||||||
lokasi: l.lokasi,
|
|
||||||
tanggalWaktu: l.tanggalWaktu,
|
|
||||||
kronologi: l.kronologi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: l.id,
|
|
||||||
judul: l.judul,
|
|
||||||
lokasi: l.lokasi,
|
|
||||||
tanggalWaktu: l.tanggalWaktu,
|
|
||||||
kronologi: l.kronologi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("laporan publik success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Penanganan Laporan...");
|
|
||||||
for (const l of penangananLaporan) {
|
|
||||||
await prisma.penangananLaporanPublik.upsert({
|
|
||||||
where: {
|
|
||||||
id: l.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
laporanId: l.laporanId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: l.id,
|
|
||||||
deskripsi: l.deskripsi,
|
|
||||||
laporanId: l.laporanId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("penanganan laporan success ...");
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const pencegahanKriminalitas = loadJsonData("keamanan/pencegahan-kriminalitas/pencegahan-kriminalitas.json");
|
|
||||||
|
|
||||||
export async function seedPencegahanKriminalitas() {
|
|
||||||
console.log("🔄 Seeding Pencegahan Kriminalitas...");
|
|
||||||
for (const d of pencegahanKriminalitas) {
|
|
||||||
await prisma.pencegahanKriminalitas.upsert({
|
|
||||||
where: {
|
|
||||||
id: d.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
judul: d.judul,
|
|
||||||
deskripsi: d.deskripsi,
|
|
||||||
deskripsiSingkat: d.deskripsiSingkat,
|
|
||||||
linkVideo: d.linkVideo,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: d.id,
|
|
||||||
judul: d.judul,
|
|
||||||
deskripsi: d.deskripsi,
|
|
||||||
deskripsiSingkat: d.deskripsiSingkat,
|
|
||||||
linkVideo: d.linkVideo,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Pencegahan Kriminalitas seeded successfully");
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const layananPolsek = loadJsonData("keamanan/polsek-terdekat/layanan-polsek.json");
|
|
||||||
const polsekTerdekat = loadJsonData("keamanan/polsek-terdekat/polsek-terdekat.json");
|
|
||||||
const layananToPolsek = loadJsonData("keamanan/polsek-terdekat/layanan-to-polsek.json");
|
|
||||||
|
|
||||||
export async function seedPolsekTerdekat() {
|
|
||||||
console.log("🔄 Seeding Layanan Polsek...");
|
|
||||||
for (const k of layananPolsek) {
|
|
||||||
await prisma.layananPolsek.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
nama: k.nama,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
nama: k.nama,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("layanan polsek success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Polsek Terdekat...");
|
|
||||||
for (const k of polsekTerdekat) {
|
|
||||||
await prisma.polsekTerdekat.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
nama: k.nama,
|
|
||||||
jarakKeDesa: k.jarakKeDesa,
|
|
||||||
alamat: k.alamat,
|
|
||||||
nomorTelepon: k.nomorTelepon,
|
|
||||||
jamOperasional: k.jamOperasional,
|
|
||||||
embedMapUrl: k.embedMapUrl,
|
|
||||||
namaTempatMaps: k.namaTempatMaps,
|
|
||||||
alamatMaps: k.alamatMaps,
|
|
||||||
linkPetunjukArah: k.linkPetunjukArah,
|
|
||||||
layananPolsekId: k.layananPolsekId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
nama: k.nama,
|
|
||||||
jarakKeDesa: k.jarakKeDesa,
|
|
||||||
alamat: k.alamat,
|
|
||||||
nomorTelepon: k.nomorTelepon,
|
|
||||||
jamOperasional: k.jamOperasional,
|
|
||||||
embedMapUrl: k.embedMapUrl,
|
|
||||||
namaTempatMaps: k.namaTempatMaps,
|
|
||||||
alamatMaps: k.alamatMaps,
|
|
||||||
linkPetunjukArah: k.linkPetunjukArah,
|
|
||||||
layananPolsekId: k.layananPolsekId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("polsek terdekat success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Layanan To Polsek...");
|
|
||||||
for (const k of layananToPolsek) {
|
|
||||||
await prisma.layananToPolsek.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
layananId: k.layananId,
|
|
||||||
polsekTerdekatId: k.polsekTerdekatId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
layananId: k.layananId,
|
|
||||||
polsekTerdekatId: k.polsekTerdekatId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("layanan to polsek success ...");
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const tipsKeamananJson = loadJsonData("keamanan/tips-keamanan/tips-keamanan.json");
|
|
||||||
|
|
||||||
export async function seedTipsKeamanan() {
|
|
||||||
console.log("🔄 Seeding Tips Keamanan...");
|
|
||||||
|
|
||||||
for (const p of tipsKeamananJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for tips keamanan "${p.judul}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.menuTipsKeamanan.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
judul: p.judul,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
judul: p.judul,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Tips Keamanan seeded: ${p.judul}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Tips Keamanan seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const dataBanjarJson = loadJsonData("kependudukan/data-banjar/data-banjar.json");
|
|
||||||
|
|
||||||
export async function seedDataBanjar() {
|
|
||||||
console.log("Seeding Data Banjar...");
|
|
||||||
|
|
||||||
for (const item of dataBanjarJson) {
|
|
||||||
await prisma.dataBanjar.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
nama: item.nama,
|
|
||||||
penduduk: item.penduduk,
|
|
||||||
kk: item.kk,
|
|
||||||
miskin: item.miskin,
|
|
||||||
tahun: item.tahun,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
nama: item.nama,
|
|
||||||
penduduk: item.penduduk,
|
|
||||||
kk: item.kk,
|
|
||||||
miskin: item.miskin,
|
|
||||||
tahun: item.tahun,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Banjar: ${item.nama} (${item.penduduk} penduduk)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Data Banjar seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const dinamikaPendudukJson = loadJsonData("kependudukan/dinamika-penduduk/dinamika-penduduk.json");
|
|
||||||
|
|
||||||
export async function seedDinamikaPenduduk() {
|
|
||||||
console.log("Seeding Dinamika Penduduk...");
|
|
||||||
|
|
||||||
for (const item of dinamikaPendudukJson) {
|
|
||||||
await prisma.dinamikaPenduduk.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
tahun: item.tahun,
|
|
||||||
kelahiran: item.kelahiran,
|
|
||||||
kematian: item.kematian,
|
|
||||||
masuk: item.masuk,
|
|
||||||
keluar: item.keluar,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
tahun: item.tahun,
|
|
||||||
kelahiran: item.kelahiran,
|
|
||||||
kematian: item.kematian,
|
|
||||||
masuk: item.masuk,
|
|
||||||
keluar: item.keluar,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Tahun ${item.tahun}: ${item.kelahiran} kelahiran, ${item.kematian} kematian`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Dinamika Penduduk seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const distribusiAgamaJson = loadJsonData("kependudukan/distribusi-agama/distribusi-agama.json");
|
|
||||||
|
|
||||||
export async function seedDistribusiAgama() {
|
|
||||||
console.log("Seeding Distribusi Agama...");
|
|
||||||
|
|
||||||
for (const item of distribusiAgamaJson) {
|
|
||||||
await prisma.distribusiAgama.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
agama: item.agama,
|
|
||||||
jumlah: item.jumlah,
|
|
||||||
tahun: item.tahun,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
agama: item.agama,
|
|
||||||
jumlah: item.jumlah,
|
|
||||||
tahun: item.tahun,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` ${item.agama}: ${item.jumlah} penganut`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Distribusi Agama seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const distribusiUmurJson = loadJsonData("kependudukan/distribusi-umur/distribusi-umur.json");
|
|
||||||
|
|
||||||
export async function seedDistribusiUmur() {
|
|
||||||
console.log("Seeding Distribusi Umur...");
|
|
||||||
|
|
||||||
for (const item of distribusiUmurJson) {
|
|
||||||
await prisma.distribusiUmur.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
rentangUmur: item.rentangUmur,
|
|
||||||
jumlah: item.jumlah,
|
|
||||||
tahun: item.tahun,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
rentangUmur: item.rentangUmur,
|
|
||||||
jumlah: item.jumlah,
|
|
||||||
tahun: item.tahun,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Rentang ${item.rentangUmur}: ${item.jumlah} jiwa`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Distribusi Umur seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const migrasiPendudukJson = loadJsonData("kependudukan/migrasi-penduduk/migrasi-penduduk.json");
|
|
||||||
|
|
||||||
export async function seedMigrasiPenduduk() {
|
|
||||||
console.log("Seeding MigrASI PENDUDUK...");
|
|
||||||
|
|
||||||
for (const item of migrasiPendudukJson) {
|
|
||||||
await prisma.migrasiPenduduk.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
nama: item.nama,
|
|
||||||
jenis: item.jenis,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
asal: item.asal,
|
|
||||||
tujuan: item.tujuan,
|
|
||||||
alasan: item.alasan,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
nama: item.nama,
|
|
||||||
jenis: item.jenis,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
asal: item.asal,
|
|
||||||
tujuan: item.tujuan,
|
|
||||||
alasan: item.alasan,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` ${item.nama}: ${item.jenis} (${item.alasan})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Migrasi Penduduk seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const artikelJson = loadJsonData("kesehatan/artikel-kesehatan/artikel-kesehatan.json");
|
|
||||||
const introJson = loadJsonData("kesehatan/artikel-kesehatan/introduction.json");
|
|
||||||
const symptomJson = loadJsonData("kesehatan/artikel-kesehatan/symptom.json");
|
|
||||||
const preventionJson = loadJsonData("kesehatan/artikel-kesehatan/prevention.json");
|
|
||||||
const firstAidJson = loadJsonData("kesehatan/artikel-kesehatan/first-aid.json");
|
|
||||||
const mythVsFactJson = loadJsonData("kesehatan/artikel-kesehatan/myth-vs-fact.json");
|
|
||||||
const doctorSignJson = loadJsonData("kesehatan/artikel-kesehatan/doctor-sign.json");
|
|
||||||
|
|
||||||
export async function seedArtikelKesehatan() {
|
|
||||||
console.log("Seeding Introduction...");
|
|
||||||
for (const item of introJson) {
|
|
||||||
await prisma.introduction.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { content: item.content },
|
|
||||||
create: { id: item.id, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Symptom...");
|
|
||||||
for (const item of symptomJson) {
|
|
||||||
await prisma.symptom.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { title: item.title, content: item.content },
|
|
||||||
create: { id: item.id, title: item.title, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Prevention...");
|
|
||||||
for (const item of preventionJson) {
|
|
||||||
await prisma.prevention.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { title: item.title, content: item.content },
|
|
||||||
create: { id: item.id, title: item.title, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding First Aid...");
|
|
||||||
for (const item of firstAidJson) {
|
|
||||||
await prisma.firstAid.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { title: item.title, content: item.content },
|
|
||||||
create: { id: item.id, title: item.title, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Myth vs Fact...");
|
|
||||||
for (const item of mythVsFactJson) {
|
|
||||||
await prisma.mythVsFact.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
title: item.title,
|
|
||||||
mitos: item.mitos,
|
|
||||||
fakta: item.fakta,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
title: item.title,
|
|
||||||
mitos: item.mitos,
|
|
||||||
fakta: item.fakta,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Doctor Sign...");
|
|
||||||
for (const item of doctorSignJson) {
|
|
||||||
await prisma.doctorSign.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { content: item.content },
|
|
||||||
create: { id: item.id, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Artikel Kesehatan...");
|
|
||||||
for (const item of artikelJson) {
|
|
||||||
await prisma.artikelKesehatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
title: item.title,
|
|
||||||
content: item.content,
|
|
||||||
introductionId: item.introductionId,
|
|
||||||
symptomId: item.symptomId,
|
|
||||||
preventionId: item.preventionId,
|
|
||||||
firstAidId: item.firstAidId,
|
|
||||||
mythVsFactId: item.mythVsFactId,
|
|
||||||
doctorSignId: item.doctorSignId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
title: item.title,
|
|
||||||
content: item.content,
|
|
||||||
introductionId: item.introductionId,
|
|
||||||
symptomId: item.symptomId,
|
|
||||||
preventionId: item.preventionId,
|
|
||||||
firstAidId: item.firstAidId,
|
|
||||||
mythVsFactId: item.mythVsFactId,
|
|
||||||
doctorSignId: item.doctorSignId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Artikel: ${item.title}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Artikel Kesehatan seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const faskesJson = loadJsonData("kesehatan/fasilitas-kesehatan/fasilitas-kesehatan.json");
|
|
||||||
const infoUmumJson = loadJsonData("kesehatan/fasilitas-kesehatan/informasi-umum.json");
|
|
||||||
const layananUnggulanJson = loadJsonData("kesehatan/fasilitas-kesehatan/layanan-unggulan.json");
|
|
||||||
const dokterJson = loadJsonData("kesehatan/fasilitas-kesehatan/dokter-tenaga-medis.json");
|
|
||||||
const fasilitasPendukungJson = loadJsonData("kesehatan/fasilitas-kesehatan/fasilitas-pendukung.json");
|
|
||||||
const prosedurJson = loadJsonData("kesehatan/fasilitas-kesehatan/prosedur-pendaftaran.json");
|
|
||||||
const tarifJson = loadJsonData("kesehatan/fasilitas-kesehatan/tarif-layanan.json");
|
|
||||||
|
|
||||||
export async function seedFasilitasKesehatan() {
|
|
||||||
console.log("Seeding Informasi Umum...");
|
|
||||||
for (const item of infoUmumJson) {
|
|
||||||
await prisma.informasiUmum.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
fasilitas: item.fasilitas,
|
|
||||||
alamat: item.alamat,
|
|
||||||
jamOperasional: item.jamOperasional,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
fasilitas: item.fasilitas,
|
|
||||||
alamat: item.alamat,
|
|
||||||
jamOperasional: item.jamOperasional,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Informasi Umum: ${item.fasilitas}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Layanan Unggulan...");
|
|
||||||
for (const item of layananUnggulanJson) {
|
|
||||||
await prisma.layananUnggulan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { content: item.content },
|
|
||||||
create: { id: item.id, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Fasilitas Pendukung...");
|
|
||||||
for (const item of fasilitasPendukungJson) {
|
|
||||||
await prisma.fasilitasPendukung.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { content: item.content },
|
|
||||||
create: { id: item.id, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Prosedur Pendaftaran...");
|
|
||||||
for (const item of prosedurJson) {
|
|
||||||
await prisma.prosedurPendaftaran.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { content: item.content },
|
|
||||||
create: { id: item.id, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Tarif dan Layanan...");
|
|
||||||
for (const item of tarifJson) {
|
|
||||||
await prisma.tarifDanLayanan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { layanan: item.layanan, tarif: item.tarif },
|
|
||||||
create: { id: item.id, layanan: item.layanan, tarif: item.tarif },
|
|
||||||
});
|
|
||||||
console.log(` Tarif: ${item.layanan}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Dokter dan Tenaga Medis...");
|
|
||||||
for (const item of dokterJson) {
|
|
||||||
await prisma.dokterdanTenagaMedis.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
name: item.name,
|
|
||||||
specialist: item.specialist,
|
|
||||||
jadwal: item.jadwal,
|
|
||||||
jadwalLibur: item.jadwalLibur,
|
|
||||||
jamBukaOperasional: item.jamBukaOperasional,
|
|
||||||
jamTutupOperasional: item.jamTutupOperasional,
|
|
||||||
jamBukaLibur: item.jamBukaLibur,
|
|
||||||
jamTutupLibur: item.jamTutupLibur,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
specialist: item.specialist,
|
|
||||||
jadwal: item.jadwal,
|
|
||||||
jadwalLibur: item.jadwalLibur,
|
|
||||||
jamBukaOperasional: item.jamBukaOperasional,
|
|
||||||
jamTutupOperasional: item.jamTutupOperasional,
|
|
||||||
jamBukaLibur: item.jamBukaLibur,
|
|
||||||
jamTutupLibur: item.jamTutupLibur,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Dokter: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Fasilitas Kesehatan...");
|
|
||||||
for (const item of faskesJson) {
|
|
||||||
await prisma.fasilitasKesehatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
name: item.name,
|
|
||||||
informasiUmumId: item.informasiUmumId,
|
|
||||||
layananUnggulanId: item.layananUnggulanId,
|
|
||||||
fasilitasPendukungId: item.fasilitasPendukungId,
|
|
||||||
prosedurPendaftaranId: item.prosedurPendaftaranId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
informasiUmumId: item.informasiUmumId,
|
|
||||||
layananUnggulanId: item.layananUnggulanId,
|
|
||||||
fasilitasPendukungId: item.fasilitasPendukungId,
|
|
||||||
prosedurPendaftaranId: item.prosedurPendaftaranId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Fasilitas Kesehatan: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Fasilitas Kesehatan seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const infoWabahPenyakitJson = loadJsonData("kesehatan/infowabahpenyakit/infowabahpenyakit.json");
|
|
||||||
|
|
||||||
export async function seedInfoWabahPenyakit() {
|
|
||||||
console.log("🔄 Seeding Info Wabah Penyakit...");
|
|
||||||
|
|
||||||
for (const p of infoWabahPenyakitJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for info wabah penyakit "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.infoWabahPenyakit.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsiSingkat: p.deskripsiSingkat,
|
|
||||||
deskripsiLengkap: p.deskripsiLengkap,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsiSingkat: p.deskripsiSingkat,
|
|
||||||
deskripsiLengkap: p.deskripsiLengkap,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Info wabah penyakit seeded: ${p.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Info wabah penyakit seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const jadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/jadwal-kegiatan.json");
|
|
||||||
const infoJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/informasi-jadwal.json");
|
|
||||||
const deskJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/deskripsi-jadwal.json");
|
|
||||||
const layananJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/layanan-jadwal.json");
|
|
||||||
const syaratJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/syarat-ketentuan.json");
|
|
||||||
const dokumenJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/dokumen-jadwal.json");
|
|
||||||
const daftarJadwalJson = loadJsonData("kesehatan/jadwal-kegiatan/pendaftaran-jadwal.json");
|
|
||||||
|
|
||||||
export async function seedJadwalKegiatan() {
|
|
||||||
console.log("Seeding Informasi Jadwal Kegiatan...");
|
|
||||||
for (const item of infoJadwalJson) {
|
|
||||||
await prisma.informasiJadwalKegiatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
name: item.name,
|
|
||||||
tanggal: item.tanggal,
|
|
||||||
waktu: item.waktu,
|
|
||||||
lokasi: item.lokasi,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
tanggal: item.tanggal,
|
|
||||||
waktu: item.waktu,
|
|
||||||
lokasi: item.lokasi,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Informasi: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Deskripsi Jadwal Kegiatan...");
|
|
||||||
for (const item of deskJadwalJson) {
|
|
||||||
await prisma.deskripsiJadwalKegiatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { deskripsi: item.deskripsi },
|
|
||||||
create: { id: item.id, deskripsi: item.deskripsi },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Layanan Jadwal Kegiatan...");
|
|
||||||
for (const item of layananJadwalJson) {
|
|
||||||
await prisma.layananJadwalKegiatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { content: item.content },
|
|
||||||
create: { id: item.id, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Syarat & Ketentuan Jadwal...");
|
|
||||||
for (const item of syaratJadwalJson) {
|
|
||||||
await prisma.syaratKetentuanJadwalKegiatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { content: item.content },
|
|
||||||
create: { id: item.id, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Dokumen Jadwal Kegiatan...");
|
|
||||||
for (const item of dokumenJadwalJson) {
|
|
||||||
await prisma.dokumenJadwalKegiatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: { content: item.content },
|
|
||||||
create: { id: item.id, content: item.content },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Pendaftaran Jadwal Kegiatan...");
|
|
||||||
for (const item of daftarJadwalJson) {
|
|
||||||
await prisma.pendaftaranJadwalKegiatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
name: item.name,
|
|
||||||
tanggal: item.tanggal,
|
|
||||||
namaOrangtua: item.namaOrtu,
|
|
||||||
nomor: item.nomor,
|
|
||||||
alamat: item.alamat,
|
|
||||||
catatan: item.catatan,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
tanggal: item.tanggal,
|
|
||||||
namaOrangtua: item.namaOrtu,
|
|
||||||
nomor: item.nomor,
|
|
||||||
alamat: item.alamat,
|
|
||||||
catatan: item.catatan,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Pendaftaran: ${item.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Jadwal Kegiatan...");
|
|
||||||
for (const item of jadwalJson) {
|
|
||||||
await prisma.jadwalKegiatan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
content: item.content,
|
|
||||||
informasiJadwalKegiatanId: item.informasiJadwalKegiatanId,
|
|
||||||
deskripsiJadwalKegiatanId: item.deskripsiJadwalKegiatanId,
|
|
||||||
layananJadwalKegiatanId: item.layananJadwalKegiatanId,
|
|
||||||
syaratKetentuanJadwalKegiatanId: item.syaratKetentuanJadwalKegiatanId,
|
|
||||||
dokumenJadwalKegiatanId: item.dokumenJadwalKegiatanId,
|
|
||||||
pendaftaranJadwalKegiatanId: item.pendaftaranJadwalKegiatanId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
content: item.content,
|
|
||||||
informasiJadwalKegiatanId: item.informasiJadwalKegiatanId,
|
|
||||||
deskripsiJadwalKegiatanId: item.deskripsiJadwalKegiatanId,
|
|
||||||
layananJadwalKegiatanId: item.layananJadwalKegiatanId,
|
|
||||||
syaratKetentuanJadwalKegiatanId: item.syaratKetentuanJadwalKegiatanId,
|
|
||||||
dokumenJadwalKegiatanId: item.dokumenJadwalKegiatanId,
|
|
||||||
pendaftaranJadwalKegiatanId: item.pendaftaranJadwalKegiatanId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Jadwal Kegiatan seeded`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Jadwal Kegiatan seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
import prisma from "@/lib/prisma";
|
|
||||||
|
|
||||||
const kontakDaruratJson = loadJsonData("kesehatan/kontak-darurat/kontak-darurat.json");
|
|
||||||
|
|
||||||
export async function seedKontakDarurat() {
|
|
||||||
console.log("🔄 Seeding Kontak Darurat...");
|
|
||||||
|
|
||||||
for (const p of kontakDaruratJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for kontak darurat "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.kontakDarurat.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
whatsapp: p.whatsapp,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
whatsapp: p.whatsapp,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Kontak darurat seeded: ${p.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Kontak darurat seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const penangananDaruratJson = loadJsonData("kesehatan/penanganan-darurat/penganan-darurat.json");
|
|
||||||
|
|
||||||
export async function seedPenangananDarurat() {
|
|
||||||
console.log("🔄 Seeding Penanganan Darurat...");
|
|
||||||
|
|
||||||
for (const p of penangananDaruratJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for penanganan darurat "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.penangananDarurat.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Penanganan darurat seeded: ${p.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Penanganan darurat seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const posyanduJson = loadJsonData("kesehatan/posyandu/posyandu.json");
|
|
||||||
|
|
||||||
export async function seedPosyandu() {
|
|
||||||
console.log("🔄 Seeding Posyandu...");
|
|
||||||
|
|
||||||
for (const p of posyanduJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for posyandu "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.posyandu.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
nomor: p.nomor,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
jadwalPelayanan: p.jadwalPelayanan,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
nomor: p.nomor,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
jadwalPelayanan: p.jadwalPelayanan,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Posyandu seeded: ${p.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Posyandu seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const programKesehatanJson = loadJsonData("kesehatan/program-kesehatan/program-kesehatan.json");
|
|
||||||
|
|
||||||
export async function seedProgramKesehatan() {
|
|
||||||
for (const p of programKesehatanJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for program kesehatan "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.programKesehatan.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsiSingkat: p.deskripsiSingkat,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsiSingkat: p.deskripsiSingkat,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Program kesehatan seeded: ${p.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const puskesmasJson = loadJsonData("kesehatan/puskesmas/puskesmas.json");
|
|
||||||
const kontakPuskesmasJson = loadJsonData("kesehatan/puskesmas/kontak-puskesmas/kontak.json");
|
|
||||||
const jamPuskesmasJson = loadJsonData("kesehatan/puskesmas/jam-puskesmas/jam.json");
|
|
||||||
|
|
||||||
export async function seedPuskesmas() {
|
|
||||||
console.log("🔄 Seeding Kontak Puskesmas...");
|
|
||||||
for (const k of kontakPuskesmasJson) {
|
|
||||||
await prisma.kontakPuskesmas.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
kontakPuskesmas: k.kontakPuskesmas,
|
|
||||||
email: k.email,
|
|
||||||
facebook: k.facebook,
|
|
||||||
kontakUGD: k.kontakUGD,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
kontakPuskesmas: k.kontakPuskesmas,
|
|
||||||
email: k.email,
|
|
||||||
facebook: k.facebook,
|
|
||||||
kontakUGD: k.kontakUGD,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("kontak puskesmas success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Jam Puskesmas...");
|
|
||||||
for (const k of jamPuskesmasJson) {
|
|
||||||
await prisma.jamOperasional.upsert({
|
|
||||||
where: {
|
|
||||||
id: k.id,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
workDays: k.workDays,
|
|
||||||
weekDays: k.weekDays,
|
|
||||||
holiday: k.holiday,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
workDays: k.workDays,
|
|
||||||
weekDays: k.weekDays,
|
|
||||||
holiday: k.holiday,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("jam puskesmas success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Puskesmas...");
|
|
||||||
|
|
||||||
for (const p of puskesmasJson) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for puskesmas "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.puskesmas.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
alamat: p.alamat,
|
|
||||||
jamId: p.jamId,
|
|
||||||
kontakId: p.kontakId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
alamat: p.alamat,
|
|
||||||
jamId: p.jamId,
|
|
||||||
kontakId: p.kontakId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Puskesmas seeded: ${p.name}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Puskesmas seed selesai");
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const grafikKepuasanJson = loadJsonData("kesehatan/grafik-kepuasan/grafik-kepuasan.json");
|
|
||||||
|
|
||||||
export async function seedGrafikKepuasan() {
|
|
||||||
console.log("Seeding Grafik Kepuasan...");
|
|
||||||
|
|
||||||
for (const item of grafikKepuasanJson) {
|
|
||||||
await prisma.grafikKepuasan.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
nama: item.nama,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
jenisKelamin: item.jenisKelamin,
|
|
||||||
alamat: item.alamat,
|
|
||||||
penyakit: item.penyakit,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
nama: item.nama,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
jenisKelamin: item.jenisKelamin,
|
|
||||||
alamat: item.alamat,
|
|
||||||
penyakit: item.penyakit,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Grafik Kepuasan: ${item.nama}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Grafik Kepuasan seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../load-json";
|
|
||||||
|
|
||||||
const kelahiranJson = loadJsonData("kesehatan/kelahiran/kelahiran.json");
|
|
||||||
const kematianJson = loadJsonData("kesehatan/kematian/kematian.json");
|
|
||||||
const dataKematianKelahiranJson = loadJsonData("kesehatan/kematian-kelahiran/data-kematian-kelahiran.json");
|
|
||||||
|
|
||||||
export async function seedKelahiranKematian() {
|
|
||||||
console.log("Seeding Kelahiran...");
|
|
||||||
for (const item of kelahiranJson) {
|
|
||||||
await prisma.kelahiran.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
nama: item.nama,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
jenisKelamin: item.jenisKelamin,
|
|
||||||
alamat: item.alamat,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
nama: item.nama,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
jenisKelamin: item.jenisKelamin,
|
|
||||||
alamat: item.alamat,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Kelahiran: ${item.nama}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Kematian...");
|
|
||||||
for (const item of kematianJson) {
|
|
||||||
await prisma.kematian.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
nama: item.nama,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
jenisKelamin: item.jenisKelamin,
|
|
||||||
alamat: item.alamat,
|
|
||||||
penyebab: item.penyebab,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
nama: item.nama,
|
|
||||||
tanggal: new Date(item.tanggal),
|
|
||||||
jenisKelamin: item.jenisKelamin,
|
|
||||||
alamat: item.alamat,
|
|
||||||
penyebab: item.penyebab,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(` Kematian: ${item.nama}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Seeding Data Kematian-Kelahiran...");
|
|
||||||
for (const item of dataKematianKelahiranJson) {
|
|
||||||
await prisma.dataKematian_Kelahiran.upsert({
|
|
||||||
where: { id: item.id },
|
|
||||||
update: {
|
|
||||||
kematianId: item.kematianId,
|
|
||||||
kelahiranId: item.kelahiranId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: item.id,
|
|
||||||
kematianId: item.kematianId,
|
|
||||||
kelahiranId: item.kelahiranId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Kelahiran & Kematian seed selesai");
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const kategoriDesaAntiKorupsi = loadJsonData("landing-page/desa-anti-korupsi/kategoriDesaAntiKorupsi.json");
|
|
||||||
const desaAntiKorupsi = loadJsonData("landing-page/desa-anti-korupsi/desaantiKorpusi.json");
|
|
||||||
|
|
||||||
export async function seedDesaAntiKorupsi() {
|
|
||||||
for (const k of kategoriDesaAntiKorupsi) {
|
|
||||||
await prisma.kategoriDesaAntiKorupsi.upsert({
|
|
||||||
where: { id: k.id },
|
|
||||||
update: {
|
|
||||||
name: k.name,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: k.id,
|
|
||||||
name: k.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("kategori desa anti korupsi success ...");
|
|
||||||
|
|
||||||
// =========== DESA ANTI KORUPSI ===========
|
|
||||||
for (const p of desaAntiKorupsi) {
|
|
||||||
await prisma.desaAntiKorupsi.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
kategoriId: p.kategoriId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
deskripsi: p.deskripsi,
|
|
||||||
kategoriId: p.kategoriId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("desa anti korupsi success ...");
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const prestasiDesa = loadJsonData("landing-page/prestasi-desa/prestasi-desa.json");
|
|
||||||
const kategoriPrestasiDesa = loadJsonData("landing-page/prestasi-desa/kategori-prestasi.json");
|
|
||||||
|
|
||||||
export async function seedPrestasiDesa() {
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Kategori Prestasi Desa...");
|
|
||||||
for (const c of kategoriPrestasiDesa) {
|
|
||||||
await prisma.kategoriPrestasiDesa.upsert({
|
|
||||||
where: { id: c.id },
|
|
||||||
update: {
|
|
||||||
name: c.name,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("kategori prestasi desa success ...");
|
|
||||||
|
|
||||||
console.log("🔄 Seeding Prestasi Desa...");
|
|
||||||
for (const m of prestasiDesa) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (m.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: m.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for prestasi desa "${m.name}": ${m.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.prestasiDesa.upsert({
|
|
||||||
where: { id: m.id },
|
|
||||||
update: {
|
|
||||||
name: m.name,
|
|
||||||
deskripsi: m.deskripsi,
|
|
||||||
kategoriId: m.kategoriId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: m.id,
|
|
||||||
name: m.name,
|
|
||||||
deskripsi: m.deskripsi,
|
|
||||||
kategoriId: m.kategoriId,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("prestasi desa success ...");
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const mediaSosial = loadJsonData("landing-page/profile/mediaSosial.json");
|
|
||||||
|
|
||||||
export async function seedMediaSosial() {
|
|
||||||
console.log("🔄 Seeding Media Sosial...");
|
|
||||||
for (const m of mediaSosial) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (m.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: m.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for berita "${m.name}": ${m.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.mediaSosial.upsert({
|
|
||||||
where: { id: m.id },
|
|
||||||
update: {
|
|
||||||
name: m.name,
|
|
||||||
iconUrl: m.iconUrl,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: m.id,
|
|
||||||
name: m.name,
|
|
||||||
iconUrl: m.iconUrl,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("media sosial success ...");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const profilePejabatDesa = loadJsonData("landing-page/profile/profile.json");
|
|
||||||
|
|
||||||
export async function seedProfileLP() {
|
|
||||||
console.log("🔄 Seeding Pejabat Desa...");
|
|
||||||
for (const p of profilePejabatDesa) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (p.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: p.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for profile "${p.name}": ${p.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.pejabatDesa.upsert({
|
|
||||||
where: { id: p.id },
|
|
||||||
update: {
|
|
||||||
name: p.name,
|
|
||||||
position: p.position,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: p.id,
|
|
||||||
name: p.name,
|
|
||||||
position: p.position,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("✅ Pejabat Desa seeding completed");
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const programInovasi = loadJsonData("landing-page/profile/programInovasi.json");
|
|
||||||
|
|
||||||
export async function seedProgramInovasi() {
|
|
||||||
console.log("🔄 Seeding Program Inovasi...");
|
|
||||||
|
|
||||||
for (const b of programInovasi) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (b.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: b.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for program inovasi "${b.name}": ${b.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.programInovasi.upsert({
|
|
||||||
where: { id: b.id },
|
|
||||||
update: {
|
|
||||||
name: b.name,
|
|
||||||
description: b.description,
|
|
||||||
link: b.link,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: b.id,
|
|
||||||
name: b.name,
|
|
||||||
description: b.description,
|
|
||||||
link: b.link,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✅ Program Inovasi seeded: ${b.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import prisma from "@/lib/prisma";
|
|
||||||
import { loadJsonData } from "../../../load-json";
|
|
||||||
|
|
||||||
const sdgsDesa = loadJsonData("landing-page/sdgs-desa/sdgs-desa.json");
|
|
||||||
|
|
||||||
export async function seedSDGSDesa() {
|
|
||||||
console.log("🔄 Seeding SDGS Desa...");
|
|
||||||
for (const m of sdgsDesa) {
|
|
||||||
let imageId: string | null = null;
|
|
||||||
|
|
||||||
if (m.imageName) {
|
|
||||||
const image = await prisma.fileStorage.findUnique({
|
|
||||||
where: { name: m.imageName },
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!image) {
|
|
||||||
console.warn(
|
|
||||||
`⚠️ Image not found for sdgs desa "${m.name}": ${m.imageName}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
imageId = image.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.sdgsDesa.upsert({
|
|
||||||
where: { id: m.id },
|
|
||||||
update: {
|
|
||||||
name: m.name,
|
|
||||||
jumlah: m.jumlah,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
id: m.id,
|
|
||||||
name: m.name,
|
|
||||||
jumlah: m.jumlah,
|
|
||||||
imageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("sdgs desa success ...");
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user