Compare commits

...

16 Commits

Author SHA1 Message Date
947adc1537 fix: production build CSS dan responsive layout untuk staging
- Tambah scripts/build.ts untuk build CSS via PostCSS/Tailwind
- Update package.json build script untuk gunakan build script baru
- Fix responsive grid di sosial-page (lg -> md breakpoint)
- Tambah padding responsive untuk mobile display
- Convert inline styles ke Tailwind classes untuk konsistensi
- Update tailwind.config.js content paths
- Tambah CSS variables di index.css untuk color palette
- Update Dockerfile untuk gunakan build script baru

Fixes: tampilan berantakan di staging karena CSS tidak ter-build dengan benar

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-12 12:16:57 +08:00
66d207c081 feat: refactor UI components to TailwindCSS with dark mode support
- Convert Mantine-based components to TailwindCSS + Recharts
- Add dark mode support for all dashboard pages
- Update routing to allow public dashboard access
- Components refactored:
  - kinreja-divisi.tsx: Village performance dashboard
  - pengaduan-layanan-publik.tsx: Public complaint management
  - jenna-analytic.tsx: Chatbot analytics dashboard
  - demografi-pekerjaan.tsx: Demographic analytics
  - keuangan-anggaran.tsx: APBDes financial dashboard
  - bumdes-page.tsx: UMKM sales monitoring
  - sosial-page.tsx: Village social monitoring
- Remove landing page, redirect / to /dashboard
- Update auth middleware for public dashboard access

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-11 15:26:16 +08:00
b77f6e8fa3 feat: update APBDes data with real values and fix SDGS layout
- Update APBDes data with actual values (390M, 470M, 290M)
- Fix SDGS Desa layout to display horizontally with auto-wrap
- Add responsive grid for SDGS cards (1 col mobile, 2 tablet, 3 desktop)
- Add GitHub OAuth redirectURI configuration

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-10 16:37:33 +08:00
9e6734d1a5 Add .env.example template with environment variables documentation
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-10 13:55:14 +08:00
github-actions[bot]
1b9ddf0f4b chore: sync workflows from base-template 2026-03-10 04:39:12 +00:00
a0f440f6b3 Docker File 2026-03-10 12:36:33 +08:00
1f56dd7660 First Deploy 2026-03-10 10:24:45 +08:00
1a2a213d0a Ganti Image Logo 2026-02-27 14:57:01 +08:00
1ec10fe623 Fix seed-2 2026-02-26 16:30:22 +08:00
226b0880e6 Fix seed 2026-02-26 16:22:08 +08:00
5d9be8c479 Fix sign in github 2026-02-26 15:07:15 +08:00
e83bea2bc2 Fix sign in, sign out, dan register localhost:3000 2026-02-26 14:48:55 +08:00
95c08681a7 fix localhost 2026-02-25 15:51:48 +08:00
9b015ec84d fix localhost 2026-02-25 15:44:26 +08:00
38b22dd2dd feat: update dashboard components (dashboard-content, help-page, kinerja-divisi)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-23 10:32:24 +08:00
5801eb4596 feat: improve header responsiveness and update seed initialization
- Add text truncation for title on mobile screens
- Hide user info section on mobile, show simplified icons only
- Update seed.ts to create admin and demo users with proper password hashing
- Add bcryptjs for password hashing in seed script
- Update QWEN.md documentation with seed command and default users

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-19 10:14:21 +08:00
63 changed files with 5550 additions and 4160 deletions

19
.env.example Normal file
View File

@@ -0,0 +1,19 @@
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/dashboard_desa?schema=public"
# Authentication
BETTER_AUTH_SECRET="your-secret-key-here-min-32-characters"
ADMIN_EMAIL="admin@example.com"
ADMIN_PASSWORD="admin123"
# GitHub OAuth (Optional)
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Application
PORT=3000
NODE_ENV=development
LOG_LEVEL=info
# Public URL
VITE_PUBLIC_URL="http://localhost:3000"

106
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,106 @@
name: Publish Docker to GHCR
on:
workflow_dispatch:
inputs:
stack_env:
description: "stack env"
required: true
type: choice
default: "dev"
options:
- dev
- prod
- stg
tag:
description: "Image tag (e.g. 1.0.0)"
required: true
default: "1.0.0"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
runs-on: ubuntu-latest
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
permissions:
contents: read
packages: write
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
df -h
- name: Checkout branch ${{ github.event.inputs.stack_env }}
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.stack_env }}
- name: Checkout scripts from main
uses: actions/checkout@v4
with:
ref: main
path: .ci
sparse-checkout: .github/workflows/script
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}
type=raw,value=${{ github.event.inputs.stack_env }}-latest
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
no-cache: true
- name: Notify success
if: success()
run: bash ./.ci/.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: success
NOTIFY_WORKFLOW: "Publish Docker"
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"
- name: Notify failure
if: failure()
run: bash ./.ci/.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: failure
NOTIFY_WORKFLOW: "Publish Docker"
NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}"

60
.github/workflows/re-pull.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: Re-Pull Docker
on:
workflow_dispatch:
inputs:
stack_name:
description: "stack name"
required: true
type: string
stack_env:
description: "stack env"
required: true
type: choice
default: "dev"
options:
- dev
- stg
- prod
jobs:
publish:
name: Re-Pull Docker ${{ github.event.inputs.stack_name }}
runs-on: ubuntu-latest
environment: ${{ vars.PORTAINER_ENV || 'portainer' }}
permissions:
contents: read
packages: write
steps:
- name: Checkout scripts from main
uses: actions/checkout@v4
with:
ref: main
sparse-checkout: .github/workflows/script
- name: Deploy ke Portainer
run: bash ./.github/workflows/script/re-pull.sh
env:
PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }}
PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }}
PORTAINER_URL: ${{ secrets.PORTAINER_URL }}
STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}
- name: Notify success
if: success()
run: bash ./.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: success
NOTIFY_WORKFLOW: "Re-Pull Docker"
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"
- name: Notify failure
if: failure()
run: bash ./.github/workflows/script/notify.sh
env:
TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
NOTIFY_STATUS: failure
NOTIFY_WORKFLOW: "Re-Pull Docker"
NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}"

26
.github/workflows/script/notify.sh vendored Normal file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}"
: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}"
: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}"
: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}"
if [ "$NOTIFY_STATUS" = "success" ]; then
ICON="✅"
TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!"
else
ICON="❌"
TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!"
fi
if [ -n "$NOTIFY_DETAIL" ]; then
TEXT="${TEXT}
${NOTIFY_DETAIL}"
fi
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg chat_id "$TELEGRAM_CHAT_ID" \
--arg text "$TEXT" \
'{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')"

93
.github/workflows/script/re-pull.sh vendored Normal file
View File

@@ -0,0 +1,93 @@
#!/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}"
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!"
echo " Pastikan nama stack sudah benar."
exit 1
fi
STACK_ID=$(echo "$STACK" | jq -r .Id)
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
ENV=$(echo "$STACK" | jq '.Env // []')
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 "🚀 Redeploying $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 container running..."
MAX_RETRY=15
COUNT=0
while [ $COUNT -lt $MAX_RETRY ]; do
sleep 5
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}")
TOTAL=$(echo "$CONTAINERS" | jq 'length')
RUNNING=$(echo "$CONTAINERS" | jq '[.[] | select(.State == "running")] | length')
FAILED=$(echo "$CONTAINERS" | jq '[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not))] | length')
echo "🔄 [${COUNT}/${MAX_RETRY}] Running: ${RUNNING} | Failed: ${FAILED} | Total: ${TOTAL}"
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status)"'
if [ "$FAILED" -gt "0" ]; then
echo ""
echo "❌ Ada container yang crash!"
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not)) | " → \(.Names[0]) | \(.Status)"'
exit 1
fi
if [ "$RUNNING" -gt "0" ]; then
echo ""
echo "✅ Stack $STACK_NAME berhasil di-redeploy dan running!"
exit 0
fi
done
echo ""
echo "❌ Timeout! Stack tidak kunjung running setelah $((MAX_RETRY * 5)) detik."
exit 1

4
.gitignore vendored
View File

@@ -16,6 +16,7 @@ _.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files # dotenv environment variable files
# Only .env.example is allowed to be committed
.env .env
.env.development.local .env.development.local
.env.test.local .env.test.local
@@ -33,6 +34,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config # Finder (MacOS) folder config
.DS_Store .DS_Store
# Dashboard-MD
Dashboard-MD
# Playwright artifacts # Playwright artifacts
test-results/ test-results/
playwright-report/ playwright-report/

62
Dockerfile Normal file
View File

@@ -0,0 +1,62 @@
# Stage 1: Build
FROM oven/bun:1.3 AS build
# Install build dependencies for native modules
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory
WORKDIR /app
# Copy package files
COPY package.json bun.lock* ./
# Install dependencies
RUN bun install --frozen-lockfile
# Copy the rest of the application code
COPY . .
# Use .env.example as default env for build
RUN cp .env.example .env
# Generate Prisma client
RUN bun x prisma generate
# Generate API types
RUN bun run gen:api
# Build the application frontend using our custom build script
RUN bun run build
# Stage 2: Runtime
FROM oven/bun:1.3-slim AS runtime
# Set environment variables
ENV NODE_ENV=production
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Set the working directory
WORKDIR /app
# Copy necessary files from build stage
COPY --from=build /app/package.json ./
COPY --from=build /app/tsconfig.json ./
COPY --from=build /app/dist ./dist
COPY --from=build /app/generated ./generated
COPY --from=build /app/src ./src
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/prisma ./prisma
# Expose the port
EXPOSE 3000
# Start the application
CMD ["bun", "start"]

12
QWEN.md
View File

@@ -51,11 +51,13 @@ bun install
```bash ```bash
cp .env.example .env cp .env.example .env
# Fill in your DATABASE_URL and BETTER_AUTH_SECRET # Fill in your DATABASE_URL and BETTER_AUTH_SECRET
# Optional: Set ADMIN_EMAIL and ADMIN_PASSWORD for admin user
``` ```
### Database Initialization ### Database Initialization
```bash ```bash
bun x prisma migrate dev bun x prisma migrate dev
bun run seed
``` ```
### Start Development ### Start Development
@@ -109,4 +111,12 @@ bun run dev
- `test:e2e`: Runs end-to-end tests - `test:e2e`: Runs end-to-end tests
- `build`: Builds the application for production - `build`: Builds the application for production
- `start`: Starts the production server - `start`: Starts the production server
- `seed`: Seeds the database with initial data - `seed`: Seeds the database with admin and demo users
## Default Users (after running `bun run seed`)
- **Admin**: `ADMIN_EMAIL` (from env) / `ADMIN_PASSWORD` (default: `admin123`)
- **Demo Users**:
- `demo1@example.com` / `demo123` (role: user)
- `demo2@example.com` / `demo123` (role: user)
- `moderator@example.com` / `demo123` (role: moderator)

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@@ -47,6 +47,7 @@
"@tabler/icons-react": "^3.36.1", "@tabler/icons-react": "^3.36.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/react-router": "^1.158.1", "@tanstack/react-router": "^1.158.1",
"bcryptjs": "^3.0.3",
"better-auth": "^1.4.18", "better-auth": "^1.4.18",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"cmdk": "^1.0.1", "cmdk": "^1.0.1",
@@ -80,6 +81,7 @@
"@tanstack/react-router-devtools": "^1.158.1", "@tanstack/react-router-devtools": "^1.158.1",
"@tanstack/router-cli": "1.158.1", "@tanstack/router-cli": "1.158.1",
"@tanstack/router-vite-plugin": "^1.158.1", "@tanstack/router-vite-plugin": "^1.158.1",
"@types/bcryptjs": "^3.0.0",
"@types/bun": "latest", "@types/bun": "latest",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
@@ -628,6 +630,8 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="],
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
@@ -776,6 +780,8 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
"bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="],
"better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="], "better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="],
"better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="],

View File

@@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts", "dev": "lsof -ti:3000 | xargs kill -9 2>/dev/null || true; bun run gen:api && REACT_EDITOR=antigravity bun --hot src/index.ts",
"lint": "biome check .", "lint": "biome check .",
"check": "biome check --write .", "check": "biome check --write .",
"format": "biome format --write .", "format": "biome format --write .",
@@ -12,7 +12,7 @@
"test": "bun test __tests__/api", "test": "bun test __tests__/api",
"test:ui": "bun test --ui __tests__/api", "test:ui": "bun test --ui __tests__/api",
"test:e2e": "bun run build && playwright test", "test:e2e": "bun run build && playwright test",
"build": "bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*'", "build": "bun run scripts/build.ts",
"start": "NODE_ENV=production bun src/index.ts", "start": "NODE_ENV=production bun src/index.ts",
"seed": "bun prisma/seed.ts" "seed": "bun prisma/seed.ts"
}, },
@@ -59,6 +59,7 @@
"@tabler/icons-react": "^3.36.1", "@tabler/icons-react": "^3.36.1",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/react-router": "^1.158.1", "@tanstack/react-router": "^1.158.1",
"bcryptjs": "^3.0.3",
"better-auth": "^1.4.18", "better-auth": "^1.4.18",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"cmdk": "^1.0.1", "cmdk": "^1.0.1",
@@ -92,6 +93,7 @@
"@tanstack/react-router-devtools": "^1.158.1", "@tanstack/react-router-devtools": "^1.158.1",
"@tanstack/router-cli": "1.158.1", "@tanstack/router-cli": "1.158.1",
"@tanstack/router-vite-plugin": "^1.158.1", "@tanstack/router-vite-plugin": "^1.158.1",
"@types/bcryptjs": "^3.0.0",
"@types/bun": "latest", "@types/bun": "latest",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",

View File

@@ -1,13 +1,16 @@
import "dotenv/config"; import "dotenv/config";
import { hash } from "bcryptjs";
import { generateId } from "better-auth";
import { prisma } from "@/utils/db"; import { prisma } from "@/utils/db";
async function seedAdminUser() { async function seedAdminUser() {
// Load environment variables // Load environment variables
const adminEmail = process.env.ADMIN_EMAIL; const adminEmail = process.env.ADMIN_EMAIL;
const adminPassword = process.env.ADMIN_PASSWORD || "admin123";
if (!adminEmail) { if (!adminEmail) {
console.log( console.log(
"No ADMIN_EMAIL environment variable found. Skipping admin role assignment.", "No ADMIN_EMAIL environment variable found. Skipping admin user creation.",
); );
return; return;
} }
@@ -30,9 +33,35 @@ async function seedAdminUser() {
console.log(`User with email ${adminEmail} already has admin role.`); console.log(`User with email ${adminEmail} already has admin role.`);
} }
} else { } else {
console.log( // Create new admin user
`No user found with email ${adminEmail}. Skipping admin role assignment.`, const hashedPassword = await hash(adminPassword, 12);
); const userId = generateId();
await prisma.user.create({
data: {
id: userId,
email: adminEmail,
name: "Admin User",
role: "admin",
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
},
});
await prisma.account.create({
data: {
id: generateId(),
userId,
accountId: userId,
providerId: "credential",
password: hashedPassword,
createdAt: new Date(),
updatedAt: new Date(),
},
});
console.log(`Admin user created with email: ${adminEmail}`);
} }
} catch (error) { } catch (error) {
console.error("Error seeding admin user:", error); console.error("Error seeding admin user:", error);
@@ -40,15 +69,82 @@ async function seedAdminUser() {
} }
} }
async function seedDemoUsers() {
const demoUsers = [
{ email: "demo1@example.com", name: "Demo User 1", role: "user" },
{ email: "demo2@example.com", name: "Demo User 2", role: "user" },
{
email: "moderator@example.com",
name: "Moderator User",
role: "moderator",
},
];
for (const userData of demoUsers) {
try {
const existingUser = await prisma.user.findUnique({
where: { email: userData.email },
});
if (!existingUser) {
const userId = generateId();
const hashedPassword = await hash("demo123", 12);
await prisma.user.create({
data: {
id: userId,
email: userData.email,
name: userData.name,
role: userData.role,
emailVerified: true,
createdAt: new Date(),
updatedAt: new Date(),
},
});
await prisma.account.create({
data: {
id: generateId(),
userId,
accountId: userId,
providerId: "credential",
password: hashedPassword,
createdAt: new Date(),
updatedAt: new Date(),
},
});
console.log(`Demo user created: ${userData.email}`);
} else {
console.log(`Demo user already exists: ${userData.email}`);
}
} catch (error) {
console.error(`Error seeding user ${userData.email}:`, error);
}
}
}
async function main() { async function main() {
console.log("Seeding database..."); console.log("Seeding database...");
await seedAdminUser(); await seedAdminUser();
await seedDemoUsers();
console.log("Database seeding completed."); console.log("Database seeding completed.");
} }
main().catch((error) => { // Only auto-execute when run directly (not when imported)
console.error("Error during seeding:", error); const isMainModule =
process.exit(1); typeof require !== "undefined"
}); ? require.main === module
: import.meta.path.endsWith("seed.ts");
if (isMainModule) {
main().catch((error) => {
console.error("Error during seeding:", error);
process.exit(1);
});
}
// Export for programmatic use
export { seedAdminUser, seedDemoUsers, main as runSeed };

BIN
public/SDGS-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/SDGS-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
public/SDGS-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
public/SDGS-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
public/logo-desa-plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

58
scripts/build.ts Normal file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bun
/**
* Build script for production
* 1. Build CSS with PostCSS/Tailwind
* 2. Bundle JS with Bun (without CSS)
* 3. Replace CSS reference in HTML
*/
import { $ } from "bun";
import fs from "node:fs";
import postcss from "postcss";
import tailwindcss from "@tailwindcss/postcss";
import autoprefixer from "autoprefixer";
console.log("🔨 Starting production build...");
// Ensure dist directory exists
if (!fs.existsSync("./dist")) {
fs.mkdirSync("./dist", { recursive: true });
}
// Step 1: Build CSS with PostCSS
console.log("🎨 Building CSS...");
const cssInput = fs.readFileSync("./src/index.css", "utf-8");
const cssResult = await postcss([tailwindcss(), autoprefixer()]).process(
cssInput,
{
from: "./src/index.css",
to: "./dist/index.css",
},
);
fs.writeFileSync("./dist/index.css", cssResult.css);
console.log("✅ CSS built successfully!");
// Step 2: Build JS with Bun (build HTML too, we'll fix CSS link later)
console.log("📦 Bundling JavaScript...");
await $`bun build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='"production"' --env='VITE_*'`;
// Step 3: Copy public assets
console.log("📁 Copying public assets...");
if (fs.existsSync("./public")) {
await $`cp -r public/* dist/ 2>/dev/null || true`;
}
// Step 4: Ensure HTML references the correct CSS
// Bun build might have renamed the CSS, we want to use our own index.css
console.log("🔧 Fixing HTML CSS reference...");
const htmlPath = "./dist/index.html";
if (fs.existsSync(htmlPath)) {
let html = fs.readFileSync(htmlPath, "utf-8");
// Replace any bundled CSS reference with our index.css
html = html.replace(/href="[^"]*\.css"/g, 'href="/index.css"');
fs.writeFileSync(htmlPath, html);
}
console.log("✅ Build completed successfully!");

View File

@@ -1,296 +1,589 @@
import {
IconBuildingStore,
IconCategory,
IconCurrency,
IconUsers,
IconTrendingUp,
IconTrendingDown,
IconChevronDown,
} from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
import { useState } from "react"; import { useState } from "react";
import {
Card,
Grid,
GridCol,
Group,
Text,
Title,
Button,
Badge,
Table,
Stack,
Select,
useMantineColorScheme
} from "@mantine/core";
import { IconBuildingStore, IconCategory, IconCurrency, IconUsers } from "@tabler/icons-react";
const BumdesPage = () => { const BumdesPage = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
const [timeFilter, setTimeFilter] = useState<string>("bulan");
// Sample data for KPI cards
const kpiData = [
{
title: "UMKM Aktif",
value: 45,
icon: <IconUsers size={24} />,
color: "darmasaba-blue",
},
{
title: "UMKM Terdaftar",
value: 68,
icon: <IconBuildingStore size={24} />,
color: "darmasaba-success",
},
{
title: "Omzet",
value: "Rp 48.000.000",
icon: <IconCurrency size={24} />,
color: "darmasaba-warning",
},
{
title: "Kategori UMKM",
value: 34,
icon: <IconCategory size={24} />,
color: "darmasaba-danger",
},
];
// Sample data for top products const [timeFilter, setTimeFilter] = useState<string>("bulan");
const topProducts = [ const [categoryFilter, setCategoryFilter] = useState<string>("semua");
{
rank: 1,
name: "Beras Premium Organik",
umkmOwner: "Warung Pak Joko",
growth: "+12%",
},
{
rank: 2,
name: "Keripik Singkong",
umkmOwner: "Ibu Sari Snack",
growth: "+8%",
},
{
rank: 3,
name: "Madu Alami",
umkmOwner: "Peternakan Lebah",
growth: "+5%",
},
];
// Sample data for product sales // KPI Data
const productSales = [ const kpiData = [
{ {
produk: "Beras Premium Organik", title: "UMKM Aktif",
penjualanBulanIni: "Rp 8.500.000", value: "45",
bulanLalu: "Rp 8.500.000", subtitle: "Beroperasi",
trend: 10, icon: IconUsers,
volume: "650 Kg", },
stok: "850 Kg", {
}, title: "UMKM Terdaftar",
{ value: "68",
produk: "Keripik Singkong", subtitle: "Total terdaftar",
penjualanBulanIni: "Rp 4.200.000", icon: IconBuildingStore,
bulanLalu: "Rp 3.800.000", },
trend: 10, {
volume: "320 Kg", title: "Omzet",
stok: "120 Kg", value: "48 JT",
}, subtitle: "Bulan ini",
{ icon: IconCurrency,
produk: "Madu Alami", },
penjualanBulanIni: "Rp 3.750.000", {
bulanLalu: "Rp 4.100.000", title: "Kategori UMKM",
trend: -8, value: "34",
volume: "150 Liter", subtitle: "Jenis produk",
stok: "45 Liter", icon: IconCategory,
}, },
{ ];
produk: "Kecap Tradisional",
penjualanBulanIni: "Rp 2.800.000",
bulanLalu: "Rp 2.500.000",
trend: 12,
volume: "280 Botol",
stok: "95 Botol",
},
];
return ( // Mini stats data
<Stack gap="lg"> const miniStats = [
{/* KPI Cards */} {
<Grid gutter="md"> title: "Total Penjualan",
{kpiData.map((kpi, index) => ( value: "Rp 30.900.000",
<GridCol key={index} span={{ base: 12, sm: 6, md: 3 }}> subtitle: "+18% vs bulan lalu",
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> isPositive: true,
<Group justify="space-between" align="center"> },
<Stack gap={0}> {
<Text size="sm" c={dark ? "dark.3" : "dimmed"}> title: "Produk Aktif",
{kpi.title} value: "7",
</Text> subtitle: "Kategori produk",
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}> },
{typeof kpi.value === 'number' ? kpi.value.toLocaleString() : kpi.value} {
</Text> title: "Total Transaksi",
</Stack> value: "500",
<Badge subtitle: "Transaksi bulan ini",
variant="light" },
color={kpi.color} ];
p={8}
radius="md"
>
{kpi.icon}
</Badge>
</Group>
</Card>
</GridCol>
))}
</Grid>
{/* Update Penjualan Produk Header */} // Top 3 products data
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> const topProducts = [
<Group justify="space-between" align="center" px="md" py="xs"> {
<Title order={3} c={dark ? "dark.0" : "black"}> rank: 1,
Update Penjualan Produk name: "Beras Premium Organik",
</Title> umkmOwner: "Kelompok Tani Subak",
<Group> sales: "Rp 8.500.000",
<Button volume: "650 Kg Terjual",
variant={timeFilter === "minggu" ? "filled" : "light"} growth: "+15%",
onClick={() => setTimeFilter("minggu")} },
color="darmasaba-blue" {
> rank: 2,
Minggu ini name: "Keripik Singkong",
</Button> umkmOwner: "Ibu Sari Snack",
<Button sales: "Rp 4.200.000",
variant={timeFilter === "bulan" ? "filled" : "light"} volume: "320 Kg Terjual",
onClick={() => setTimeFilter("bulan")} growth: "+8%",
color="darmasaba-blue" },
> {
Bulan ini rank: 3,
</Button> name: "Madu Alami",
</Group> umkmOwner: "Peternakan Lebah",
</Group> sales: "Rp 3.750.000",
</Card> volume: "150 Liter Terjual",
growth: "+5%",
},
];
<Grid gutter="md"> // Product sales data
{/* Produk Unggulan (Left Column) */} const productSales = [
<GridCol span={{ base: 12, lg: 4 }}> {
<Stack gap="md"> produk: "Beras Premium Organik",
{/* Total Penjualan, Produk Aktif, Total Transaksi */} umkm: "Kelompok Tani Subak",
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> penjualanBulanIni: "Rp 8.500.000",
<Stack gap="md"> bulanLalu: "Rp 7.400.000",
<Group justify="space-between"> trend: 15,
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Total Penjualan</Text> volume: "650 Kg",
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>Rp 28.500.000</Text> stok: "850 Kg",
</Group> },
<Group justify="space-between"> {
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Produk Aktif</Text> produk: "Keripik Singkong",
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>124 Produk</Text> umkm: "Ibu Sari Snack",
</Group> penjualanBulanIni: "Rp 4.200.000",
<Group justify="space-between"> bulanLalu: "Rp 3.800.000",
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Total Transaksi</Text> trend: 10,
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>1.240 Transaksi</Text> volume: "320 Kg",
</Group> stok: "120 Kg",
</Stack> },
</Card> {
produk: "Madu Alami",
umkm: "Peternakan Lebah",
penjualanBulanIni: "Rp 3.750.000",
bulanLalu: "Rp 4.100.000",
trend: -8,
volume: "150 Liter",
stok: "45 Liter",
},
{
produk: "Kecap Tradisional",
umkm: "Bu Darmi",
penjualanBulanIni: "Rp 2.800.000",
bulanLalu: "Rp 2.500.000",
trend: 12,
volume: "280 Botol",
stok: "95 Botol",
},
];
{/* Top 3 Produk Terlaris */} const cardStyle = {
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> backgroundColor: dark ? "#1E293B" : "white",
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>Top 3 Produk Terlaris</Title> border: `1px solid ${dark ? "#1E293B" : "white"}`,
<Stack gap="sm"> };
{topProducts.map((product) => (
<Group key={product.rank} justify="space-between" align="center">
<Group gap="sm">
<Badge
variant="filled"
color={product.rank === 1 ? "gold" : product.rank === 2 ? "gray" : "bronze"}
radius="xl"
size="lg"
>
{product.rank}
</Badge>
<Stack gap={0}>
<Text fw={500} c={dark ? "dark.0" : "black"}>{product.name}</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{product.umkmOwner}</Text>
</Stack>
</Group>
<Badge
variant="light"
color={product.growth.startsWith('+') ? "green" : "red"}
>
{product.growth}
</Badge>
</Group>
))}
</Stack>
</Card>
</Stack>
</GridCol>
{/* Detail Penjualan Produk (Right Column) */} const textStyle = {
<GridCol span={{ base: 12, lg: 8 }}> color: dark ? "white" : "#1F2937",
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> };
<Group justify="space-between" mb="md">
<Title order={4} c={dark ? "dark.0" : "black"}>Detail Penjualan Produk</Title> const subtitleStyle = {
<Select color: dark ? "#9CA3AF" : "#6B7280",
placeholder="Filter kategori" };
data={[
{ value: 'semua', label: 'Semua Kategori' }, return (
{ value: 'makanan', label: 'Makanan' }, <div
{ value: 'minuman', label: 'Minuman' }, className="min-h-screen"
{ value: 'kerajinan', label: 'Kerajinan' }, style={{
]} backgroundColor: dark ? "#0F172A" : "#F3F4F6",
defaultValue="semua" minHeight: "100vh",
w={200} padding: "1.5rem",
/> }}
</Group> >
<div
<Table striped highlightOnHover withColumnBorders> className="max-w-7xl mx-auto"
<Table.Thead> style={{
<Table.Tr> maxWidth: "80rem",
<Table.Th><Text c={dark ? "white" : "dimmed"}>Produk</Text></Table.Th> marginLeft: "auto",
<Table.Th><Text c={dark ? "white" : "dimmed"}>Penjualan Bulan Ini</Text></Table.Th> marginRight: "auto",
<Table.Th><Text c={dark ? "white" : "dimmed"}>Bulan Lalu</Text></Table.Th> }}
<Table.Th><Text c={dark ? "white" : "dimmed"}>Trend</Text></Table.Th> >
<Table.Th><Text c={dark ? "white" : "dimmed"}>Volume</Text></Table.Th> {/* Row 1: Top 4 Metrics Cards */}
<Table.Th><Text c={dark ? "white" : "dimmed"}>Stok</Text></Table.Th> <div
<Table.Th><Text c={dark ? "white" : "dimmed"}>Aksi</Text></Table.Th> className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
</Table.Tr> style={{
</Table.Thead> display: "grid",
<Table.Tbody> gridTemplateColumns: "repeat(4, 1fr)",
{productSales.map((product, index) => ( gap: "1.5rem",
<Table.Tr key={index}> marginBottom: "1.5rem",
<Table.Td> }}
<Text fw={500} c={dark ? "dark.0" : "black"}>{product.produk}</Text> >
</Table.Td> {kpiData.map((kpi, index) => (
<Table.Td> <div
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>{product.penjualanBulanIni}</Text> key={index}
</Table.Td> className="rounded-xl shadow-sm p-6"
<Table.Td> style={{
<Text fz={"sm"} c={dark ? "white" : "dimmed"}>{product.bulanLalu}</Text> ...cardStyle,
</Table.Td> borderRadius: "12px",
<Table.Td> boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
<Group gap="xs"> padding: "1.5rem",
<Text c={product.trend >= 0 ? "green" : "red"}> }}
{product.trend >= 0 ? '↑' : '↓'} {Math.abs(product.trend)}% >
</Text> <div className="flex items-center justify-between">
</Group> <div className="flex-1">
</Table.Td> <h3
<Table.Td> className="text-sm font-medium mb-1"
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>{product.volume}</Text> style={subtitleStyle}
</Table.Td> >
<Table.Td> {kpi.title}
<Badge </h3>
variant="light" <p
color={parseInt(product.stok) > 200 ? "green" : "yellow"} className="text-3xl font-bold mb-1"
> style={textStyle}
{product.stok} >
</Badge> {kpi.value}
</Table.Td> </p>
<Table.Td> <p
<Button variant="subtle" size="compact-sm" color="darmasaba-blue"> className="text-xs"
Detail style={subtitleStyle}
</Button> >
</Table.Td> {kpi.subtitle}
</Table.Tr> </p>
))} </div>
</Table.Tbody> <div className="flex-shrink-0 ml-4">
</Table> <div
</Card> className="w-12 h-12 rounded-full flex items-center justify-center text-white"
</GridCol> style={{ backgroundColor: "#1F3A5F" }}
</Grid> >
</Stack> <kpi.icon size={24} />
); </div>
</div>
</div>
</div>
))}
</div>
{/* Row 2: Sales Update Header */}
<div
className="rounded-xl shadow-sm mb-6 overflow-hidden"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
marginBottom: "1.5rem",
}}
>
<div
className="px-6 py-4 flex items-center justify-between"
style={{ backgroundColor: "#1F3A5F" }}
>
<h3 className="text-lg font-semibold text-white">
Update Penjualan Produk
</h3>
<div className="flex items-center gap-2">
<button
onClick={() => setTimeFilter("minggu")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
timeFilter === "minggu"
? "bg-white text-[#1F3A5F]"
: "bg-white/20 text-white hover:bg-white/30"
}`}
>
Minggu ini
</button>
<button
onClick={() => setTimeFilter("bulan")}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
timeFilter === "bulan"
? "bg-white text-[#1F3A5F]"
: "bg-white/20 text-white hover:bg-white/30"
}`}
>
Bulan ini
</button>
</div>
</div>
</div>
{/* Row 3: Main Content Grid */}
<div
className="grid grid-cols-1 lg:grid-cols-10 gap-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(10, 1fr)",
gap: "1.5rem",
}}
>
{/* Left Column (30%) */}
<div className="lg:col-span-3 space-y-6">
{/* Produk Unggulan Section */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Produk Unggulan
</h3>
{/* Mini Stats Cards */}
<div className="space-y-4 mb-6">
{miniStats.map((stat, index) => (
<div
key={index}
className="p-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<p
className="text-sm font-medium mb-1"
style={subtitleStyle}
>
{stat.title}
</p>
<p
className="text-xl font-bold"
style={textStyle}
>
{stat.value}
</p>
{stat.subtitle && (
<p
className="text-xs mt-1"
style={
stat.isPositive
? { color: "#22C55E" }
: subtitleStyle
}
>
{stat.subtitle}
</p>
)}
</div>
))}
</div>
{/* Top 3 Products */}
<h4
className="text-base font-semibold mb-4"
style={textStyle}
>
Top 3 Produk Terlaris
</h4>
<div className="space-y-4">
{topProducts.map((product) => (
<div
key={product.rank}
className="flex items-start gap-3 p-3 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-white font-bold text-sm flex-shrink-0"
style={{
backgroundColor:
product.rank === 1
? "#FFD700"
: product.rank === 2
? "#C0C0C0"
: "#CD7F32",
}}
>
#{product.rank}
</div>
<div className="flex-1">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.name}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{product.umkmOwner}
</p>
<div className="flex items-center justify-between mt-2">
<p className="text-xs font-medium" style={{ color: "#22C55E" }}>
{product.sales}
</p>
<p
className="text-xs"
style={{ color: "#22C55E" }}
>
{product.growth}
</p>
</div>
<p className="text-xs mt-1" style={subtitleStyle}>
{product.volume}
</p>
</div>
</div>
))}
</div>
</div>
</div>
{/* Right Column (70%) */}
<div className="lg:col-span-7">
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<div className="flex items-center justify-between mb-6">
<h3
className="text-lg font-semibold"
style={textStyle}
>
Detail Penjualan Produk
</h3>
<div className="relative">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
className="appearance-none px-4 py-2 pr-8 rounded-lg text-sm font-medium border-0 focus:ring-2 focus:ring-[#1F3A5F] cursor-pointer"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
color: dark ? "white" : "#1F2937",
}}
>
<option value="semua">Semua Kategori</option>
<option value="makanan">Makanan</option>
<option value="minuman">Minuman</option>
<option value="kerajinan">Kerajinan</option>
</select>
<IconChevronDown
size={16}
className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
/>
</div>
</div>
{/* Data Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr
style={{
borderBottom: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Produk
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Penjualan Bulan Ini
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Bulan Lalu
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Trend
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Volume
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Stok
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Aksi
</th>
</tr>
</thead>
<tbody>
{productSales.map((product, index) => (
<tr
key={index}
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
<td className="py-4 px-4">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.produk}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
{product.umkm}
</p>
</td>
<td className="py-4 px-4">
<p
className="text-sm font-medium"
style={textStyle}
>
{product.penjualanBulanIni}
</p>
</td>
<td className="py-4 px-4">
<p
className="text-sm"
style={subtitleStyle}
>
{product.bulanLalu}
</p>
</td>
<td className="py-4 px-4">
<div
className="flex items-center gap-1 text-sm font-medium"
style={{
color: product.trend >= 0 ? "#22C55E" : "#EF4444",
}}
>
{product.trend >= 0 ? (
<IconTrendingUp size={16} />
) : (
<IconTrendingDown size={16} />
)}
{product.trend >= 0 ? "+" : ""}
{product.trend}%
</div>
</td>
<td className="py-4 px-4">
<p
className="text-sm"
style={textStyle}
>
{product.volume}
</p>
</td>
<td className="py-4 px-4">
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium"
style={{
backgroundColor: parseInt(product.stok) > 200 ? "#DCFCE7" : "#FEE2E2",
color: parseInt(product.stok) > 200 ? "#166534" : "#991B1B",
}}
>
{product.stok}
</span>
</td>
<td className="py-4 px-4">
<button
className="px-3 py-1.5 rounded-lg text-sm font-medium transition-colors"
style={{
backgroundColor: "#1F3A5F",
color: "white",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#2d4a6f")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "#1F3A5F")
}
>
Detail
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
);
}; };
export default BumdesPage; export default BumdesPage;

View File

@@ -1,6 +1,6 @@
import type { ReactNode } from "react";
// Import Mantine components directly // Import Mantine components directly
import { Group, Text, ThemeIcon, Badge } from "@mantine/core"; import { Badge, Group, Text, ThemeIcon } from "@mantine/core";
import type { ReactNode } from "react";
// Import custom Card and its sub-components // Import custom Card and its sub-components
import { Card } from "./ui/card"; import { Card } from "./ui/card";

View File

@@ -1,4 +1,5 @@
import { import {
Briefcase,
Calendar, Calendar,
CheckCircle, CheckCircle,
FileText, FileText,
@@ -13,25 +14,27 @@ import {
Pie, Pie,
PieChart, PieChart,
ResponsiveContainer, ResponsiveContainer,
Tooltip, // Added Tooltip import
XAxis, XAxis,
YAxis, YAxis,
Tooltip, // Added Tooltip import
} from "recharts"; } from "recharts";
// Import Mantine components // Import Mantine components
import { import {
Grid,
Stack,
Group,
Text,
Title,
ActionIcon, ActionIcon,
Progress,
Box,
Badge, Badge,
ThemeIcon, Box,
Card, // Added for icon containers Card, // Added for icon containers
Grid,
Group,
Image,
Progress,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme, // Add this import useMantineColorScheme, // Add this import
} from "@mantine/core"; } from "@mantine/core";
@@ -66,15 +69,35 @@ const eventData = [
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" }, { date: "19 Oktober 2025", title: "Rapat Koordinasi" },
]; ];
const apbdesData = [
{ name: "Belanja", value: 390, label: "390M" },
{ name: "Pendapatan", value: 470, label: "470M" },
{ name: "Pembiayaan", value: 290, label: "290M" },
];
const sdgsData = [
{ label: "Desa Berenergi Bersih Dan Terbarukan", value: 99.64, image: "/SDGS-7.png" },
{ label: "Desa Damai Berkeadilan", value: 78.65, image: "/SDGS-16.png" },
{ label: "Desa Sehat Dan Sejahtera", value: 77.37, image: "/SDGS-3.png" },
{ label: "Desa Tanpa Kemiskinan", value: 52.62, image: "/SDGS-1.png" }
];
export function DashboardContent() { export function DashboardContent() {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
return ( return (
<Stack gap="lg"> <Stack gap="lg">
{/* Stats Cards */} {/* Stats Cards */}
<Grid gutter="md"> <Grid gutter="md">
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" h="100%" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
h="100%"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" align="flex-start" w="100%"> <Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}> <Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs"> <Text size="sm" c="dimmed" mb="xs">
@@ -92,14 +115,26 @@ export function DashboardContent() {
12% dari minggu lalu +12% 12% dari minggu lalu +12%
</Text> </Text>
</Box> </Box>
<ThemeIcon variant="filled" size="xl" radius="xl" color={dark ? 'gray' : 'darmasaba-blue'}> <ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-navy"}
>
<FileText style={{ width: "70%", height: "70%" }} /> <FileText style={{ width: "70%", height: "70%" }} />
</ThemeIcon> </ThemeIcon>
</Group> </Group>
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" h="100%" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
h="100%"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" align="flex-start" w="100%"> <Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}> <Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs"> <Text size="sm" c="dimmed" mb="xs">
@@ -114,14 +149,26 @@ export function DashboardContent() {
14 baru, 14 diproses 14 baru, 14 diproses
</Text> </Text>
</Box> </Box>
<ThemeIcon variant="filled" size="xl" radius="xl" color={dark ? 'gray' : 'darmasaba-blue'}> <ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-navy"}
>
<MessageCircle style={{ width: "70%", height: "70%" }} /> <MessageCircle style={{ width: "70%", height: "70%" }} />
</ThemeIcon> </ThemeIcon>
</Group> </Group>
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" h="100%" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
h="100%"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" align="flex-start" w="100%"> <Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}> <Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs"> <Text size="sm" c="dimmed" mb="xs">
@@ -139,14 +186,26 @@ export function DashboardContent() {
+8% +8%
</Text> </Text>
</Box> </Box>
<ThemeIcon variant="filled" size="xl" radius="xl" color={dark ? 'gray' : 'darmasaba-blue'}> <ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-navy"}
>
<CheckCircle style={{ width: "70%", height: "70%" }} /> <CheckCircle style={{ width: "70%", height: "70%" }} />
</ThemeIcon> </ThemeIcon>
</Group> </Group>
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, md: 6, lg: 3 }}> <Grid.Col span={{ base: 12, md: 6, lg: 3 }}>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" h="100%" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
h="100%"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" align="flex-start" w="100%"> <Group justify="space-between" align="flex-start" w="100%">
<Box style={{ flex: 1 }}> <Box style={{ flex: 1 }}>
<Text size="sm" c="dimmed" mb="xs"> <Text size="sm" c="dimmed" mb="xs">
@@ -161,7 +220,12 @@ export function DashboardContent() {
dari 482 responden dari 482 responden
</Text> </Text>
</Box> </Box>
<ThemeIcon variant="filled" size="xl" radius="xl" color={dark ? 'gray' : 'darmasaba-blue'}> <ThemeIcon
variant="filled"
size="xl"
radius="xl"
color={dark ? "gray" : "darmasaba-navy"}
>
<Users style={{ width: "70%", height: "70%" }} /> <Users style={{ width: "70%", height: "70%" }} />
</ThemeIcon> </ThemeIcon>
</Group> </Group>
@@ -171,7 +235,13 @@ export function DashboardContent() {
<Grid gutter="lg"> <Grid gutter="lg">
{/* Bar Chart */} {/* Bar Chart */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Box> <Box>
<Title order={4} mb={5}> <Title order={4} mb={5}>
@@ -222,7 +292,7 @@ export function DashboardContent() {
<Tooltip /> <Tooltip />
<Bar <Bar
dataKey="value" dataKey="value"
fill="var(--mantine-color-blue-filled)" fill="#1E3A5F"
radius={[4, 4, 0, 0]} radius={[4, 4, 0, 0]}
/> />
</BarChart> </BarChart>
@@ -232,7 +302,13 @@ export function DashboardContent() {
{/* Pie Chart */} {/* Pie Chart */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Title order={4} mb={5}> <Title order={4} mb={5}>
Tingkat Kepuasan Tingkat Kepuasan
</Title> </Title>
@@ -259,19 +335,35 @@ export function DashboardContent() {
</ResponsiveContainer> </ResponsiveContainer>
<Group justify="center" gap="md" mt="md"> <Group justify="center" gap="md" mt="md">
<Group gap="xs"> <Group gap="xs">
<Box w={12} h={12} style={{ backgroundColor: COLORS[0], borderRadius: "50%" }} /> <Box
w={12}
h={12}
style={{ backgroundColor: COLORS[0], borderRadius: "50%" }}
/>
<Text size="sm">Sangat puas (0%)</Text> <Text size="sm">Sangat puas (0%)</Text>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<Box w={12} h={12} style={{ backgroundColor: COLORS[1], borderRadius: "50%" }} /> <Box
w={12}
h={12}
style={{ backgroundColor: COLORS[1], borderRadius: "50%" }}
/>
<Text size="sm">Puas (0%)</Text> <Text size="sm">Puas (0%)</Text>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<Box w={12} h={12} style={{ backgroundColor: COLORS[2], borderRadius: "50%" }} /> <Box
w={12}
h={12}
style={{ backgroundColor: COLORS[2], borderRadius: "50%" }}
/>
<Text size="sm">Cukup (0%)</Text> <Text size="sm">Cukup (0%)</Text>
</Group> </Group>
<Group gap="xs"> <Group gap="xs">
<Box w={12} h={12} style={{ backgroundColor: COLORS[3], borderRadius: "50%" }} /> <Box
w={12}
h={12}
style={{ backgroundColor: COLORS[3], borderRadius: "50%" }}
/>
<Text size="sm">Kurang (0%)</Text> <Text size="sm">Kurang (0%)</Text>
</Group> </Group>
</Group> </Group>
@@ -283,50 +375,17 @@ export function DashboardContent() {
<Grid gutter="lg"> <Grid gutter="lg">
{/* Divisi Teraktif */} {/* Divisi Teraktif */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group gap="xs" mb="lg"> <Group gap="xs" mb="lg">
<Box> <Box>
{/* Original SVG icon */} {/* Original SVG icon */}
<svg <Briefcase color="#1E3A5F" />
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="3"
y="3"
width="7"
height="7"
rx="1"
fill="currentColor"
/>
<rect
x="3"
y="14"
width="7"
height="7"
rx="1"
fill="currentColor"
/>
<rect
x="14"
y="3"
width="7"
height="7"
rx="1"
fill="currentColor"
/>
<rect
x="14"
y="14"
width="7"
height="7"
rx="1"
fill="currentColor"
/>
</svg>
</Box> </Box>
<Title order={4}>Divisi Teraktif</Title> <Title order={4}>Divisi Teraktif</Title>
</Group> </Group>
@@ -345,7 +404,7 @@ export function DashboardContent() {
value={(divisi.value / 37) * 100} value={(divisi.value / 37) * 100}
size="sm" size="sm"
radius="xl" radius="xl"
color="blue" color="#1E3A5F"
/> />
</Box> </Box>
))} ))}
@@ -355,7 +414,13 @@ export function DashboardContent() {
{/* Kalender */} {/* Kalender */}
<Grid.Col span={{ base: 12, lg: 6 }}> <Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Group gap="xs" mb="lg"> <Group gap="xs" mb="lg">
<Calendar style={{ width: 20, height: 20 }} /> <Calendar style={{ width: 20, height: 20 }} />
<Title order={4}>Kalender & Kegiatan Mendatang</Title> <Title order={4}>Kalender & Kegiatan Mendatang</Title>
@@ -364,7 +429,10 @@ export function DashboardContent() {
{eventData.map((event, index) => ( {eventData.map((event, index) => (
<Box <Box
key={index} key={index}
style={{ borderLeft: "4px solid var(--mantine-color-blue-filled)", paddingLeft: 12 }} style={{
borderLeft: "4px solid #1E3A5F",
paddingLeft: 12,
}}
> >
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{event.date} {event.date}
@@ -378,31 +446,71 @@ export function DashboardContent() {
</Grid> </Grid>
{/* APBDes Chart */} {/* APBDes Chart */}
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}> <Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Title order={4} mb="lg"> <Title order={4} mb="lg">
Grafik APBDes Grafik APBDes
</Title> </Title>
<Stack gap="xs"> <Stack gap="xs">
<Group align="center" gap="md"> {apbdesData.map((data, index) => (
<Text size="sm" fw={500} w={60}> <Grid key={index} align="center">
Belanja <Grid.Col span={3}>
</Text> <Text size="sm" fw={500}>
<Progress value={70} size="lg" radius="xl" color="blue" style={{ flex: 1 }} /> {data.name}
</Group> </Text>
<Group align="center" gap="md"> </Grid.Col>
<Text size="sm" fw={500} w={60}> <Grid.Col span={7}>
Pendapatan <Progress
</Text> value={(data.value / 470) * 100}
<Progress value={90} size="lg" radius="xl" color="green" style={{ flex: 1 }} /> size="lg"
</Group> radius="xl"
<Group align="center" gap="md"> color="#1E3A5F"
<Text size="sm" fw={500} w={60}> />
Pembangunan </Grid.Col>
</Text> <Grid.Col span={2}>
<Progress value={50} size="lg" radius="xl" color="orange" style={{ flex: 1 }} /> <Text size="sm" fw={600} ta="right">
</Group> {data.label}
</Text>
</Grid.Col>
</Grid>
))}
</Stack> </Stack>
</Card> </Card>
{/* SDGS Desa */}
<Card
p="md"
style={{ borderColor: dark ? "#141D34" : "white" }}
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
>
<Title order={4} mb="lg">
SDGS Desa
</Title>
<SimpleGrid cols={{ base: 2, md: 5 }}>
{sdgsData.map((data, index) => (
<Card key={index} withBorder bg={dark ? "#141D34" : "white"} p="md">
<Group gap="sm" align="center">
<Image src={data.image} width={40} height={40} />
<Box>
<Text size="sm" ta={"center"} fw={500} lineClamp={2}>
{data.label}
</Text>
<Text size="sm" ta={"center"} fw={600} c="darmasaba-blue">
{data.value}
</Text>
</Box>
</Group>
</Card>
))}
</SimpleGrid>
</Card>
</Stack> </Stack>
); );
} }

View File

@@ -1,315 +1,544 @@
import React from "react";
import { import {
Card, IconArrowDown,
Title, IconArrowUp,
Text, IconBabyCarriage,
Group, IconSkull,
Stack, IconUsers,
Grid, IconHome,
Box, IconExclamationCircle,
Table, } from "@tabler/icons-react";
useMantineColorScheme, import { useMantineColorScheme } from "@mantine/core";
} from "@mantine/core"; import {
import { IconBabyCarriage, IconSkull, IconArrowUp, IconArrowDown } from "@tabler/icons-react"; Bar,
import { BarChart, PieChart } from "@mantine/charts"; BarChart,
CartesianGrid,
// Sample Data Cell,
const kpiData = [ Pie,
{ PieChart,
id: 1, ResponsiveContainer,
title: "Total Penduduk", Tooltip,
value: "5.634", XAxis,
sub: "Aktif terdaftar", YAxis,
icon: ( } from "recharts";
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon penduduk"
>
<title>Total Penduduk</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z"
/>
</svg>
),
},
{
id: 2,
title: "Kepala Keluarga",
value: "1.354",
sub: "Total KK",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kepala keluarga"
>
<title>Kepala Keluarga</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
/>
</svg>
),
},
{
id: 3,
title: "Kelahiran",
value: "23",
sub: "Tahun ini",
icon: (
<IconBabyCarriage className="h-6 w-6 text-muted-foreground" role="img" aria-label="Icon kelahiran" />
),
},
{
id: 4,
title: "Kemiskinan",
value: "324",
delta: "-10% dari tahun lalu",
deltaType: "positive",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
role="img"
aria-label="Icon kemiskinan"
>
<title>Kemiskinan</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
),
},
];
const ageDistributionData = [
{ ageRange: "17-25", total: 850 },
{ ageRange: "26-35", total: 1200 },
{ ageRange: "36-45", total: 1100 },
{ ageRange: "46-55", total: 950 },
{ ageRange: "56-65", total: 750 },
{ ageRange: "65+", total: 484 },
];
const jobDistributionData = [
{ job: "Sipil", total: 1200 },
{ job: "Guru", total: 850 },
{ job: "Petani", total: 950 },
{ job: "Pedagang", total: 750 },
{ job: "Wiraswasta", total: 984 },
];
const religionData = [
{ religion: "Hindu", total: 4234, color: "red" },
{ religion: "Islam", total: 856, color: "blue" },
{ religion: "Kristen", total: 412, color: "green" },
{ religion: "Buddha", total: 202, color: "yellow" },
];
const banjarData = [
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 },
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 },
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 },
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 },
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 },
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 },
];
const dynamicStats = [
{ title: "Kelahiran", value: "23", icon: <IconBabyCarriage size={16} />, color: "green" },
{ title: "Kematian", value: "12", icon: <IconSkull size={16} />, color: "red" },
{ title: "Pindah Masuk", value: "45", icon: <IconArrowDown size={16} />, color: "blue" },
{ title: "Pindah Keluar", value: "32", icon: <IconArrowUp size={16} />, color: "orange" },
];
const DemografiPekerjaan = () => { const DemografiPekerjaan = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Total Penduduk",
value: "5.634",
subtitle: "Aktif terdaftar",
icon: IconUsers,
},
{
id: 2,
title: "Kepala Keluarga",
value: "1.354",
subtitle: "Total KK",
icon: IconHome,
},
{
id: 3,
title: "Kelahiran",
value: "23",
subtitle: "Tahun ini",
icon: IconBabyCarriage,
},
{
id: 4,
title: "Kemiskinan",
value: "324",
subtitle: "-10% dari tahun lalu",
icon: IconExclamationCircle,
},
];
// Age distribution data
const ageDistributionData = [
{ ageRange: "17-25", total: 850 },
{ ageRange: "26-35", total: 1200 },
{ ageRange: "36-45", total: 1100 },
{ ageRange: "46-55", total: 950 },
{ ageRange: "56-65", total: 750 },
{ ageRange: "65+", total: 484 },
];
// Job distribution data
const jobDistributionData = [
{ job: "Sipil", total: 1200 },
{ job: "Guru", total: 850 },
{ job: "Petani", total: 950 },
{ job: "Pedagang", total: 750 },
{ job: "Wiraswasta", total: 984 },
];
// Religion data
const religionData = [
{ religion: "Hindu", total: 4234, color: "#EF4444" },
{ religion: "Islam", total: 856, color: "#3B82F6" },
{ religion: "Kristen", total: 412, color: "#10B981" },
{ religion: "Buddha", total: 202, color: "#F59E0B" },
];
// Banjar data
const banjarData = [
{ banjar: "Banjar Darmasaba", population: 1200, kk: 300, poor: 45 },
{ banjar: "Banjar Manesa", population: 950, kk: 240, poor: 32 },
{ banjar: "Banjar Cabe", population: 800, kk: 200, poor: 28 },
{ banjar: "Banjar Penenjoan", population: 1100, kk: 280, poor: 38 },
{ banjar: "Banjar Baler Pasar", population: 984, kk: 250, poor: 42 },
{ banjar: "Banjar Bucu", population: 600, kk: 184, poor: 25 },
];
// Dynamic stats
const dynamicStats = [
{
title: "Kelahiran",
value: "23",
icon: IconBabyCarriage,
color: "#10B981",
},
{
title: "Kematian",
value: "12",
icon: IconSkull,
color: "#EF4444",
},
{
title: "Pindah Masuk",
value: "45",
icon: IconArrowDown,
color: "#3B82F6",
},
{
title: "Pindah Keluar",
value: "32",
icon: IconArrowUp,
color: "#F59E0B",
},
];
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD", "#DBEAFE"];
const cardStyle = {
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return ( return (
<Box className="space-y-6"> <div
<Stack gap="xl"> className="min-h-screen"
{/* KPI Cards */} style={{
<Grid gutter="lg"> backgroundColor: dark ? "#10192D" : "#F3F4F6",
minHeight: "100vh",
padding: "1.5rem",
}}
>
<div
className="max-w-7xl mx-auto"
style={{
maxWidth: "80rem",
marginLeft: "auto",
marginRight: "auto",
}}
>
{/* Row 1: 4 Statistic Cards */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{kpiData.map((kpi) => ( {kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}> <div
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> key={kpi.id}
<Group justify="space-between" align="flex-start" mb="xs"> className="rounded-xl shadow-sm p-6"
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}> style={{
{kpi.title} ...cardStyle,
</Text> borderRadius: "12px",
{React.cloneElement(kpi.icon, { boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
className: "h-6 w-6", padding: "1.5rem",
color: dark ? "var(--mantine-color-dark-3)" : "var(--mantine-color-dimmed)", }}
})} >
</Group> <div className="flex items-center justify-between">
<Title order={3} fw={700} c={dark ? "dark.0" : "black"} mt="xs"> <div className="flex-1">
{kpi.value} <h3
</Title> className="text-sm font-medium mb-1"
{kpi.delta && ( style={subtitleStyle}
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: dark ? "dark.3" : "dimmed"
}
mt={4}
> >
{kpi.delta} {kpi.title}
</Text> </h3>
)} <p
{kpi.sub && ( className="text-3xl font-bold mb-1"
<Text size="xs" c={dark ? "dark.3" : "dimmed"} mt={2}> style={textStyle}
{kpi.sub} >
</Text> {kpi.value}
)} </p>
</Card> <p
</Grid.Col> className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1E3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))} ))}
</Grid> </div>
{/* Charts Section */} {/* Row 2: 2 Chart Cards */}
<Grid gutter="lg"> <div
{/* Grafik Pengelompokan Umur */} className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"
<Grid.Col span={{ base: 12, lg: 6 }}> style={{
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> display: "grid",
<Title order={3} fw={500} mb="md"> gridTemplateColumns: "repeat(2, 1fr)",
Grafik Pengelompokan Umur gap: "1.5rem",
</Title> marginBottom: "1.5rem",
<BarChart }}
h={300} >
data={ageDistributionData} {/* Age Distribution Bar Chart */}
dataKey="ageRange" <div
series={[{ name: 'total', color: 'darmasaba-navy' }]} className="rounded-xl shadow-sm p-6"
withLegend style={{
/> ...cardStyle,
</Card> borderRadius: "12px",
</Grid.Col> boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
{/* Demografi Pekerjaan */} }}
<Grid.Col span={{ base: 12, lg: 6 }}> >
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <h3
<Title order={3} fw={500} mb="md"> className="text-lg font-semibold mb-4"
Demografi Pekerjaan style={textStyle}
</Title> >
<BarChart Grafik Pengelompokan Umur
h={300} </h3>
data={jobDistributionData} <ResponsiveContainer width="100%" height={300}>
dataKey="job" <BarChart data={ageDistributionData}>
series={[{ name: 'total', color: 'darmasaba-navy' }]} <CartesianGrid
withLegend strokeDasharray="3 3"
/> vertical={false}
</Card> stroke={dark ? "#2d3748" : "#E5E7EB"}
</Grid.Col> />
</Grid> <XAxis
dataKey="ageRange"
{/* Agama & Data per Banjar */} axisLine={false}
<Grid gutter="lg"> tickLine={false}
{/* Distribusi Agama */} tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
<Grid.Col span={{ base: 12, lg: 6 }}> />
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <YAxis
<Title order={3} fw={500} mb="md"> axisLine={false}
Distribusi Agama tickLine={false}
</Title> tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
<PieChart />
h={300} <Tooltip
data={religionData.map(item => ({ contentStyle={{
name: item.religion, backgroundColor: dark ? "#1F2937" : "white",
value: item.total, border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
color: item.color borderRadius: "8px",
}))} color: dark ? "white" : "#1F2937",
withLabels }}
withLabelsLine />
labelsPosition="outside" <Bar dataKey="total" radius={[4, 4, 0, 0]}>
labelsType="percent" {ageDistributionData.map((entry, index) => (
/> <Cell
</Card> key={`cell-${index}`}
</Grid.Col> fill={COLORS[index % COLORS.length]}
/>
{/* Data per Banjar */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md">
Data per Banjar
</Title>
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th><Text c={dark ? "dark.0" : "black"}>Banjar</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.0" : "black"}>Penduduk</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.0" : "black"}>KK</Text></Table.Th>
<Table.Th><Text c={dark ? "dark.0" : "black"}>Miskin</Text></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{banjarData.map((item, index) => (
<Table.Tr key={`${item.banjar}-${index}`}>
<Table.Td><Text c={dark ? "dark.0" : "black"}>{item.banjar}</Text></Table.Td>
<Table.Td><Text c={dark ? "dark.0" : "black"}>{item.population.toLocaleString()}</Text></Table.Td>
<Table.Td><Text c={dark ? "dark.0" : "black"}>{item.kk.toLocaleString()}</Text></Table.Td>
<Table.Td>
<Text c={dark ? "red.4" : "red"}>{item.poor.toLocaleString()}</Text>
</Table.Td>
</Table.Tr>
))} ))}
</Table.Tbody> </Bar>
</Table> </BarChart>
</Card> </ResponsiveContainer>
</Grid.Col> </div>
</Grid>
{/* Statistik Dinamika Penduduk */} {/* Job Distribution Bar Chart */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div
<Title order={3} fw={500} c={dark ? "dark.0" : "black"} mb="md"> className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Demografi Pekerjaan
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={jobDistributionData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="job"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="total" radius={[4, 4, 0, 0]}>
{jobDistributionData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{/* Row 3: 3 Insight Cards */}
<div
className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{/* Religion Distribution Pie Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Distribusi Agama
</h3>
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie
data={religionData}
cx="50%"
cy="50%"
outerRadius={80}
dataKey="total"
nameKey="religion"
label={({ name, percent }) =>
`${name}: ${percent ? (percent * 100).toFixed(0) : 0}%`
}
>
{religionData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
</PieChart>
</ResponsiveContainer>
</div>
{/* Population per Banjar Table */}
<div
className="rounded-xl shadow-sm p-6 lg:col-span-2"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Data per Banjar
</h3>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#E5E7EB"}`,
}}
>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Banjar
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Penduduk
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
KK
</th>
<th
className="text-left py-3 px-4 text-sm font-medium"
style={subtitleStyle}
>
Miskin
</th>
</tr>
</thead>
<tbody>
{banjarData.map((item, index) => (
<tr
key={index}
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#F3F4F6"}`,
}}
>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.banjar}
</td>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.population.toLocaleString()}
</td>
<td
className="py-3 px-4 text-sm"
style={textStyle}
>
{item.kk.toLocaleString()}
</td>
<td
className="py-3 px-4 text-sm"
style={{ color: "#EF4444" }}
>
{item.poor.toLocaleString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
{/* Population Dynamics Stats */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Statistik Dinamika Penduduk Statistik Dinamika Penduduk
</Title> </h3>
<Grid gutter="md"> <div
className="grid grid-cols-1 md:grid-cols-4 gap-4"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1rem",
}}
>
{dynamicStats.map((stat, index) => ( {dynamicStats.map((stat, index) => (
<Grid.Col key={`${stat.title}-${index}`} span={{ base: 12, md: 3 }}> <div
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> key={index}
<Group justify="space-between" align="center"> className="p-4 rounded-lg"
<Box> style={{
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}> backgroundColor: dark ? "#1F2937" : "#F9FAFB",
{stat.title} borderRadius: "8px",
</Text> padding: "1rem",
<Title order={4} fw={700} c={stat.color}> }}
{stat.value} >
</Title> <div className="flex items-center justify-between">
</Box> <div>
<Box c={stat.color}> <p
{stat.icon} className="text-sm font-medium mb-1"
</Box> style={subtitleStyle}
</Group> >
</Card> {stat.title}
</Grid.Col> </p>
<p
className="text-2xl font-bold"
style={{ color: stat.color }}
>
{stat.value}
</p>
</div>
<div
className="w-10 h-10 rounded-full flex items-center justify-center"
style={{
backgroundColor: `${stat.color}20`,
color: stat.color,
}}
>
<stat.icon size={20} />
</div>
</div>
</div>
))} ))}
</Grid> </div>
</Card> </div>
</Stack> </div>
</Box> </div>
); );
}; };
export default DemografiPekerjaan; export default DemografiPekerjaan;

View File

@@ -1,108 +1,72 @@
import { useLocation } from "@tanstack/react-router";
import { Bell, Moon, Sun, User as UserIcon } from "lucide-react"; // Renamed User to UserIcon to avoid conflict with Mantine's User component if it exists
import { import {
ActionIcon,
Badge,
Box,
Group, Group,
Text, Text,
Title,
ActionIcon,
Divider,
Avatar,
Box,
Badge,
useMantineColorScheme, useMantineColorScheme,
} from "@mantine/core"; } from "@mantine/core";
import { useLocation } from "@tanstack/react-router";
import { Bell, Moon, Sun } from "lucide-react";
export function Header() { export function Header() {
const location = useLocation(); const location = useLocation();
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
// Define page titles based on route const title =
const getPageTitle = () => { location.pathname === "/"
switch (location.pathname) { ? "Desa Darmasaba"
case "/": : "Desa Darmasaba";
return "Desa Darmasaba";
case "/kinerja-divisi":
return "Kinerja Divisi";
case "/pengaduan":
return "Pengaduan & Layanan Publik";
case "/analytic":
return "Jenna Analytic";
case "/demografi":
return "Demografi & Kependudukan";
case "/keuangan":
return "Keuangan & Anggaran";
case "/bumdes":
return "Bumdes & UMKM Desa";
case "/sosial":
case "/keamanan":
return "Keamanan";
case "/bantuan":
return "Bantuan";
case "/pengaturan":
return "Pengaturan";
default:
return "Desa Darmasaba";
}
};
return ( return (
<Group justify="space-between" w="100%"> <Box
{/* Title */} style={{
<Title order={3} c={"white"}>{getPageTitle()}</Title> display: "grid",
gridTemplateColumns: "1fr auto 1fr",
alignItems: "center",
width: "100%",
}}
>
{/* LEFT SPACER (burger sudah di luar) */}
<Box />
{/* Right Section */} {/* CENTER TITLE */}
<Group gap="md"> <Text
c="white"
fw={600}
size="md"
style={{
textAlign: "center",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{title}
</Text>
{/* RIGHT ICONS */}
<Group gap="xs" justify="flex-end">
<ActionIcon
onClick={toggleColorScheme}
variant="subtle"
radius="xl"
>
{dark ? <Sun size={18} /> : <Moon size={18} />}
</ActionIcon>
{/* User Info */} <ActionIcon variant="subtle" radius="xl" pos="relative">
<Group gap="sm"> <Bell size={18} />
<Box ta="right"> <Badge
<Text c={"white"} size="sm" fw={500}> size="xs"
I. B. Surya Prabhawa M... color="red"
</Text> style={{ position: "absolute", top: -4, right: -4 }}
<Text c={"white"} size="xs">
Kepala Desa
</Text>
</Box>
<Avatar color="blue" radius="xl">
<UserIcon color="white" style={{ width: "70%", height: "70%" }} />
</Avatar>
</Group>
{/* Divider */}
<Divider orientation="vertical" h={30} />
{/* Icons */}
<Group gap="sm">
<ActionIcon
onClick={() => toggleColorScheme()}
variant="subtle"
size="lg"
radius="xl"
aria-label="Toggle color scheme"
> >
{dark ? ( 10
<Sun color="white" style={{ width: "70%", height: "70%" }} /> </Badge>
) : ( </ActionIcon>
<Moon color="white" style={{ width: "70%", height: "70%" }} />
)}
</ActionIcon>
<ActionIcon variant="subtle" size="lg" radius="xl" pos="relative">
<Bell color="white" style={{ width: "70%", height: "70%" }} />
<Badge
size="xs"
color="red"
variant="filled"
style={{ position: "absolute", top: 0, right: 0 }}
radius={"xl"}
>
10
</Badge>
</ActionIcon>
</Group>
</Group> </Group>
</Group> </Box>
); );
} }

View File

@@ -1,248 +1,428 @@
import { Container, Grid, Title, Text, SimpleGrid, Box, Accordion, Stack, useMantineColorScheme } from '@mantine/core'; import {
import { HelpCard } from '@/components/ui/help-card'; Accordion,
import { IconBook, IconVideo, IconHelpCircle, IconMessage, IconFileText, IconHeadphones } from '@tabler/icons-react'; Box,
import { useState } from 'react'; Container,
Grid,
SimpleGrid,
Stack,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconBook,
IconFileText,
IconHeadphones,
IconHelpCircle,
IconMessage,
IconVideo,
} from "@tabler/icons-react";
import { useState } from "react";
import { HelpCard } from "@/components/ui/help-card";
const HelpPage = () => { const HelpPage = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
// Sample data for sections // Sample data for sections
const guideItems = [ const guideItems = [
{ title: 'Cara Login', description: 'Langkah-langkah untuk login ke dashboard' }, {
{ title: 'Navigasi Dashboard', description: 'Penjelasan tentang tata letak dan navigasi' }, title: "Cara Login",
{ title: 'Fitur Dasar', description: 'Panduan penggunaan fitur-fitur utama' }, description: "Langkah-langkah untuk login ke dashboard",
{ title: 'Tips & Trik', description: 'Tips untuk meningkatkan produktivitas' }, },
]; {
title: "Navigasi Dashboard",
description: "Penjelasan tentang tata letak dan navigasi",
},
{
title: "Fitur Dasar",
description: "Panduan penggunaan fitur-fitur utama",
},
{
title: "Tips & Trik",
description: "Tips untuk meningkatkan produktivitas",
},
];
const videoItems = [ const videoItems = [
{ title: 'Dashboard Overview', duration: '5:23' }, { title: "Dashboard Overview", duration: "5:23" },
{ title: 'Analisis Data', duration: '8:45' }, { title: "Analisis Data", duration: "8:45" },
{ title: 'Membuat Laporan', duration: '6:12' }, { title: "Membuat Laporan", duration: "6:12" },
{ title: 'Export Data', duration: '4:30' }, { title: "Export Data", duration: "4:30" },
]; ];
const faqItems = [ const faqItems = [
{ question: 'Bagaimana cara reset password?', answer: 'Anda dapat mereset password melalui halaman login dengan klik "Lupa Password"' }, {
{ question: 'Apakah saya bisa mengakses data offline?', answer: 'Saat ini aplikasi hanya dapat diakses secara online' }, question: "Bagaimana cara reset password?",
{ question: 'Berapa lama waktu respon support?', answer: 'Tim support kami biasanya merespon dalam waktu kurang dari 24 jam' }, answer:
{ question: 'Bagaimana cara menambahkan pengguna baru?', answer: 'Fitur penambahan pengguna dapat ditemukan di menu Pengaturan > Manajemen Pengguna' }, 'Anda dapat mereset password melalui halaman login dengan klik "Lupa Password"',
]; },
{
question: "Apakah saya bisa mengakses data offline?",
answer: "Saat ini aplikasi hanya dapat diakses secara online",
},
{
question: "Berapa lama waktu respon support?",
answer:
"Tim support kami biasanya merespon dalam waktu kurang dari 24 jam",
},
{
question: "Bagaimana cara menambahkan pengguna baru?",
answer:
"Fitur penambahan pengguna dapat ditemukan di menu Pengaturan > Manajemen Pengguna",
},
];
const documentationItems = [ const documentationItems = [
{ title: 'API Reference', description: 'Dokumentasi lengkap untuk integrasi API' }, {
{ title: 'Integrasi Sistem', description: 'Cara mengintegrasikan dengan sistem eksternal' }, title: "API Reference",
{ title: 'Format Data', description: 'Spesifikasi format data yang didukung' }, description: "Dokumentasi lengkap untuk integrasi API",
{ title: 'Best Practices', description: 'Praktik terbaik dalam penggunaan platform' }, },
]; {
title: "Integrasi Sistem",
description: "Cara mengintegrasikan dengan sistem eksternal",
},
{
title: "Format Data",
description: "Spesifikasi format data yang didukung",
},
{
title: "Best Practices",
description: "Praktik terbaik dalam penggunaan platform",
},
];
const stats = [ const stats = [
{ value: '150+', label: 'Artikel Panduan' }, { value: "150+", label: "Artikel Panduan" },
{ value: '50+', label: 'Video Tutorial' }, { value: "50+", label: "Video Tutorial" },
{ value: '24/7', label: 'Support Aktif' }, { value: "24/7", label: "Support Aktif" },
]; ];
// State for chat functionality // State for chat functionality
const [messages, setMessages] = useState([ const [messages, setMessages] = useState([
{ id: 1, text: 'Halo! Saya Jenna, asisten virtual Anda. Bagaimana saya bisa membantu hari ini?', sender: 'jenna' } {
]); id: 1,
const [inputValue, setInputValue] = useState(''); text: "Halo! Saya Jenna, asisten virtual Anda. Bagaimana saya bisa membantu hari ini?",
const [isLoading, setIsLoading] = useState(false); sender: "jenna",
},
]);
const [inputValue, setInputValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSendMessage = () => { const handleSendMessage = () => {
if (inputValue.trim() === '') return; if (inputValue.trim() === "") return;
// Add user message // Add user message
const newUserMessage = { const newUserMessage = {
id: messages.length + 1, id: messages.length + 1,
text: inputValue, text: inputValue,
sender: 'user' sender: "user",
}; };
setMessages(prev => [...prev, newUserMessage]); setMessages((prev) => [...prev, newUserMessage]);
setInputValue(''); setInputValue("");
setIsLoading(true); setIsLoading(true);
// Simulate Jenna's response after delay // Simulate Jenna's response after delay
setTimeout(() => { setTimeout(() => {
const jennaResponse = { const jennaResponse = {
id: messages.length + 2, id: messages.length + 2,
text: 'Terima kasih atas pertanyaan Anda. Saat ini saya adalah versi awal dari asisten virtual. Tim kami sedang mengembangkan kemampuan saya lebih lanjut.', text: "Terima kasih atas pertanyaan Anda. Saat ini saya adalah versi awal dari asisten virtual. Tim kami sedang mengembangkan kemampuan saya lebih lanjut.",
sender: 'jenna' sender: "jenna",
}; };
setMessages(prev => [...prev, jennaResponse]); setMessages((prev) => [...prev, jennaResponse]);
setIsLoading(false); setIsLoading(false);
}, 1000); }, 1000);
}; };
const handleKeyPress = (e: React.KeyboardEvent) => { const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
handleSendMessage(); handleSendMessage();
} }
}; };
return ( return (
<Container size="lg" py="xl"> <Container size="lg" py="lg">
<Title order={1} mb="xl" ta="center">Pusat Bantuan</Title> {/* Statistics Section */}
<Text size="lg" color="dimmed" ta="center" mb="xl"> <SimpleGrid cols={3} spacing="lg" mb="xl">
Temukan jawaban untuk pertanyaan Anda atau hubungi tim support kami {stats.map((stat, index) => (
</Text> <HelpCard
key={index}
bg={dark ? "#141D34" : "white"}
p="lg"
style={{
textAlign: "center",
borderColor: dark ? "#141D34" : "white",
}}
h="100%"
>
<Text size="xl" fw={700} style={{ fontSize: "32px" }}>
{stat.value}
</Text>
<Text size="sm" color="dimmed">
{stat.label}
</Text>
</HelpCard>
))}
</SimpleGrid>
{/* Statistics Section */} <Stack gap="lg">
<SimpleGrid cols={3} spacing="lg" mb="xl"> <Box>
{stats.map((stat, index) => ( <Grid gutter="lg" justify="center">
<HelpCard key={index} bg={dark ? "#141D34" : "white"} p="lg" style={{ textAlign: 'center', borderColor: dark ? "#141D34" : "white" }} h="100%" > {/* Panduan Memulai */}
<Text size="xl" fw={700} style={{ fontSize: '32px' }}>{stat.value}</Text> <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<Text size="sm" color="dimmed">{stat.label}</Text> <HelpCard
</HelpCard> style={{ borderColor: dark ? "#141D34" : "white" }}
))} bg={dark ? "#141D34" : "white"}
</SimpleGrid> icon={<IconBook size={24} />}
title="Panduan Memulai"
h="100%"
>
<Box>
{guideItems.map((item, index) => (
<Box
key={index}
py="sm"
style={{
borderBottom: "1px solid #eee",
cursor: "pointer",
}}
onClick={() => alert(`Navigasi ke ${item.title}`)}
>
<Text fw={500}>{item.title}</Text>
<Text size="sm" color="dimmed">
{item.description}
</Text>
</Box>
))}
</Box>
</HelpCard>
</Grid.Col>
<Stack gap="lg"> {/* Video Tutorial */}
<Box> <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<Grid gutter="lg" justify="center"> <HelpCard
{/* Panduan Memulai */} style={{ borderColor: dark ? "#141D34" : "white" }}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> bg={dark ? "#141D34" : "white"}
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconBook size={24} />} title="Panduan Memulai" h="100%"> icon={<IconVideo size={24} />}
<Box> title="Video Tutorial"
{guideItems.map((item, index) => ( h="100%"
<Box key={index} py="sm" style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }} onClick={() => alert(`Navigasi ke ${item.title}`)}> >
<Text fw={500}>{item.title}</Text> <Box>
<Text size="sm" color="dimmed">{item.description}</Text> {videoItems.map((item, index) => (
</Box> <Box
))} key={index}
</Box> py="sm"
</HelpCard> style={{
</Grid.Col> borderBottom: "1px solid #eee",
cursor: "pointer",
}}
onClick={() => alert(`Buka video: ${item.title}`)}
>
<Text fw={500}>{item.title}</Text>
<Text size="sm" color="dimmed">
{item.duration}
</Text>
</Box>
))}
</Box>
</HelpCard>
</Grid.Col>
{/* Video Tutorial */} {/* FAQ */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconVideo size={24} />} title="Video Tutorial" h="100%"> <HelpCard
<Box> style={{ borderColor: dark ? "#141D34" : "white" }}
{videoItems.map((item, index) => ( bg={dark ? "#141D34" : "white"}
<Box key={index} py="sm" style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }} onClick={() => alert(`Buka video: ${item.title}`)}> icon={<IconHelpCircle size={24} />}
<Text fw={500}>{item.title}</Text> title="FAQ"
<Text size="sm" color="dimmed">{item.duration}</Text> h="100%"
</Box> >
))} <Accordion variant="separated">
</Box> {faqItems.map((item, index) => (
</HelpCard> <Accordion.Item
</Grid.Col> style={{
backgroundColor: dark ? "#263852ff" : "#F1F5F9",
}}
key={index}
value={`faq-${index}`}
>
<Accordion.Control>{item.question}</Accordion.Control>
<Accordion.Panel>
<Text size="sm">{item.answer}</Text>
</Accordion.Panel>
</Accordion.Item>
))}
</Accordion>
</HelpCard>
</Grid.Col>
</Grid>
</Box>
{/* FAQ */} <Box>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> <Grid>
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconHelpCircle size={24} />} title="FAQ" h="100%"> {/* Hubungi Support */}
<Accordion variant="separated" > <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
{faqItems.map((item, index) => ( <HelpCard
<Accordion.Item style={{ backgroundColor: dark ? "#263852ff" : "#F1F5F9" }} key={index} value={`faq-${index}`}> style={{ borderColor: dark ? "#141D34" : "white" }}
<Accordion.Control>{item.question}</Accordion.Control> bg={dark ? "#141D34" : "white"}
<Accordion.Panel> icon={<IconHeadphones size={24} />}
<Text size="sm">{item.answer}</Text> title="Hubungi Support"
</Accordion.Panel> h="100%"
</Accordion.Item> >
))} <Box>
</Accordion> <Text fw={500}>Email</Text>
</HelpCard> <Text size="sm" color="dimmed" mb="md">
</Grid.Col> <a href="mailto:support@example.com">support@example.com</a>
</Grid> </Text>
</Box>
<Box> <Text fw={500}>WhatsApp</Text>
<Grid> <Text size="sm" color="dimmed" mb="md">
{/* Hubungi Support */} <a href="https://wa.me/1234567890">+62 123 456 7890</a>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> </Text>
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconHeadphones size={24} />} title="Hubungi Support" h="100%">
<Box>
<Text fw={500}>Email</Text>
<Text size="sm" color="dimmed" mb="md"><a href="mailto:support@example.com">support@example.com</a></Text>
<Text fw={500}>WhatsApp</Text> <Text fw={500}>Jam Kerja</Text>
<Text size="sm" color="dimmed" mb="md"><a href="https://wa.me/1234567890">+62 123 456 7890</a></Text> <Text size="sm" color="dimmed">
Senin - Jumat, 09:00 - 17:00 WIB
</Text>
<Text fw={500}>Jam Kerja</Text> <Text fw={500} mt="md">
<Text size="sm" color="dimmed">Senin - Jumat, 09:00 - 17:00 WIB</Text> Waktu Respon
</Text>
<Text size="sm" color="dimmed">
Rata-rata 2-4 jam kerja
</Text>
</Box>
</HelpCard>
</Grid.Col>
<Text fw={500} mt="md">Waktu Respon</Text> {/* Dokumentasi */}
<Text size="sm" color="dimmed">Rata-rata 2-4 jam kerja</Text> <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
</Box> <HelpCard
</HelpCard> style={{ borderColor: dark ? "#141D34" : "white" }}
</Grid.Col> bg={dark ? "#141D34" : "white"}
icon={<IconFileText size={24} />}
title="Dokumentasi"
h="100%"
>
<Box>
{documentationItems.map((item, index) => (
<Box
key={index}
py="sm"
style={{
borderBottom: "1px solid #eee",
cursor: "pointer",
}}
onClick={() =>
alert(`Navigasi ke dokumentasi: ${item.title}`)
}
>
<Text fw={500}>{item.title}</Text>
<Text size="sm" color="dimmed">
{item.description}
</Text>
</Box>
))}
</Box>
</HelpCard>
</Grid.Col>
{/* Dokumentasi */} {/* Jenna - Virtual Assistant */}
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconFileText size={24} />} title="Dokumentasi" h="100%"> <HelpCard
<Box> style={{ borderColor: dark ? "#141D34" : "white" }}
{documentationItems.map((item, index) => ( bg={dark ? "#141D34" : "white"}
<Box key={index} py="sm" style={{ borderBottom: '1px solid #eee', cursor: 'pointer' }} onClick={() => alert(`Navigasi ke dokumentasi: ${item.title}`)}> icon={<IconMessage size={24} />}
<Text fw={500}>{item.title}</Text> title="Jenna - Virtual Assistant"
<Text size="sm" color="dimmed">{item.description}</Text> h="100%"
</Box> >
))} <Box
</Box> style={{
</HelpCard> height: "300px",
</Grid.Col> display: "flex",
flexDirection: "column",
}}
>
<Box
style={{
flex: 1,
overflowY: "auto",
marginBottom: "12px",
maxHeight: "200px",
}}
>
{messages.map((msg) => (
<Box
key={msg.id}
style={{
alignSelf:
msg.sender === "user" ? "flex-end" : "flex-start",
backgroundColor:
msg.sender === "user"
? dark
? "#263852ff"
: "#F1F5F9"
: dark
? "#263852ff"
: "#F1F5F9",
color:
msg.sender === "user"
? dark
? "#F1F5F9"
: "#263852ff"
: dark
? "#F1F5F9"
: "#263852ff",
padding: "8px 12px",
borderRadius: "8px",
marginBottom: "8px",
maxWidth: "80%",
}}
>
{msg.text}
</Box>
))}
</Box>
{/* Jenna - Virtual Assistant */} <Box style={{ display: "flex", gap: "8px" }}>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> <input
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconMessage size={24} />} title="Jenna - Virtual Assistant" h="100%"> type="text"
<Box style={{ height: '300px', display: 'flex', flexDirection: 'column' }}> value={inputValue}
<Box style={{ flex: 1, overflowY: 'auto', marginBottom: '12px', maxHeight: '200px' }}> onChange={(e) => setInputValue(e.target.value)}
{messages.map((msg) => ( onKeyPress={handleKeyPress}
<Box placeholder="Ketik pesan Anda..."
key={msg.id} style={{
style={{ flex: 1,
alignSelf: msg.sender === 'user' ? 'flex-end' : 'flex-start', padding: "8px 12px",
backgroundColor: msg.sender === 'user' ? dark ? "#263852ff" : "#F1F5F9" : dark ? "#263852ff" : "#F1F5F9", borderRadius: "20px",
color: msg.sender === 'user' ? dark ? "#F1F5F9" : "#263852ff" : dark ? "#F1F5F9" : "#263852ff", border: "1px solid #ccc",
padding: '8px 12px', }}
borderRadius: '8px', disabled={isLoading}
marginBottom: '8px', />
maxWidth: '80%' <button
}} onClick={handleSendMessage}
> disabled={isLoading || inputValue.trim() === ""}
{msg.text} style={{
</Box> padding: "8px 16px",
))} borderRadius: "20px",
</Box> backgroundColor: "#3B82F6",
color: "white",
<Box style={{ display: 'flex', gap: '8px' }}> border: "none",
<input cursor: "pointer",
type="text" }}
value={inputValue} >
onChange={(e) => setInputValue(e.target.value)} Kirim
onKeyPress={handleKeyPress} </button>
placeholder="Ketik pesan Anda..." </Box>
style={{ </Box>
flex: 1, </HelpCard>
padding: '8px 12px', </Grid.Col>
borderRadius: '20px', </Grid>
border: '1px solid #ccc', </Box>
}} </Stack>
disabled={isLoading} </Container>
/> );
<button
onClick={handleSendMessage}
disabled={isLoading || inputValue.trim() === ''}
style={{
padding: '8px 16px',
borderRadius: '20px',
backgroundColor: '#3B82F6',
color: 'white',
border: 'none',
cursor: 'pointer',
}}
>
Kirim
</button>
</Box>
</Box>
</HelpCard>
</Grid.Col>
</Grid>
</Box>
</Stack>
</Container>
);
}; };
export default HelpPage; export default HelpPage;

View File

@@ -1,262 +1,328 @@
import React from "react";
import { import {
Button, IconAlertTriangle,
Card, IconClock,
Badge, IconMessageChatbot,
Progress, IconSparkles,
Title, } from "@tabler/icons-react";
Text, import { useMantineColorScheme } from "@mantine/core";
Group, import {
Stack, Bar,
Grid, BarChart,
Box, CartesianGrid,
useMantineColorScheme, Cell,
} from "@mantine/core"; ResponsiveContainer,
import { BarChart } from "@mantine/charts"; Tooltip,
XAxis,
// Sample Data YAxis,
const kpiData = [ } from "recharts";
{
id: 1,
title: "Interaksi Hari Ini",
value: "61",
delta: "+15% dari kemarin",
deltaType: "positive",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H16.5m-13.5 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"
/>
</svg>
),
},
{
id: 2,
title: "Jawaban Otomatis",
value: "87%",
sub: "53 dari 61 interaksi",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
/>
</svg>
),
},
{
id: 3,
title: "Belum Ditindak",
value: "8",
sub: "Perlu respon manual",
deltaType: "negative",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
),
},
{
id: 4,
title: "Waktu Respon",
value: "2.3 sec",
sub: "Rata-rata",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
),
},
];
const chartData = [
{ day: "Sen", total: 100 },
{ day: "Sel", total: 120 },
{ day: "Rab", total: 90 },
{ day: "Kam", total: 150 },
{ day: "Jum", total: 110 },
{ day: "Sab", total: 80 },
{ day: "Min", total: 130 },
];
const topTopics = [
{ topic: "Cara mengurus KTP", count: 89 },
{ topic: "Syarat Kartu Keluarga", count: 76 },
{ topic: "Jadwal Posyandu", count: 64 },
{ topic: "Pengaduan jalan rusak", count: 52 },
{ topic: "Info program bansos", count: 48 },
];
const busyHours = [
{ period: "Pagi (0812)", percentage: 30 },
{ period: "Siang (1216)", percentage: 40 },
{ period: "Sore (1620)", percentage: 20 },
{ period: "Malam (2008)", percentage: 10 },
];
const JennaAnalytic = () => { const JennaAnalytic = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Interaksi Hari Ini",
value: "61",
subtitle: "+15% dari kemarin",
icon: IconMessageChatbot,
},
{
id: 2,
title: "Jawaban Otomatis",
value: "87%",
subtitle: "53 dari 61 interaksi",
icon: IconSparkles,
},
{
id: 3,
title: "Belum Ditindak",
value: "8",
subtitle: "Perlu respon manual",
icon: IconAlertTriangle,
},
{
id: 4,
title: "Waktu Respon",
value: "2.3s",
subtitle: "Rata-rata",
icon: IconClock,
},
];
// Weekly chatbot interaction data
const weeklyData = [
{ day: "Sen", interactions: 100 },
{ day: "Sel", interactions: 120 },
{ day: "Rab", interactions: 90 },
{ day: "Kam", interactions: 150 },
{ day: "Jum", interactions: 110 },
{ day: "Sab", interactions: 80 },
{ day: "Min", interactions: 130 },
];
// Top topics data
const topTopics = [
{ topic: "Cara mengurus KTP", count: 89 },
{ topic: "Syarat Kartu Keluarga", count: 76 },
{ topic: "Jadwal Posyandu", count: 64 },
{ topic: "Pengaduan jalan rusak", count: 52 },
{ topic: "Info program bansos", count: 48 },
];
// Busy hour distribution
const busyHours = [
{ period: "Pagi (0812)", percentage: 30 },
{ period: "Siang (1216)", percentage: 40 },
{ period: "Sore (1620)", percentage: 20 },
{ period: "Malam (2008)", percentage: 10 },
];
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD"];
const cardStyle = {
backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return ( return (
<Box className="space-y-6"> <div
<Stack gap="xl"> className="min-h-screen"
{/* KPI Cards */} style={{
<Grid gutter="lg"> backgroundColor: dark ? "#10192D" : "#F3F4F6",
minHeight: "100vh",
padding: "1.5rem",
}}
>
<div
className="max-w-7xl mx-auto"
style={{
maxWidth: "80rem",
marginLeft: "auto",
marginRight: "auto",
}}
>
{/* Row 1: 4 Statistic Cards */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{kpiData.map((kpi) => ( {kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}> <div
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> key={kpi.id}
<Group justify="space-between" align="flex-start" mb="xs"> className="rounded-xl shadow-sm p-6"
<Text size="sm" fw={500} c="dimmed"> style={{
{kpi.title} ...cardStyle,
</Text> borderRadius: "12px",
{React.cloneElement(kpi.icon, { boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
className: "h-6 w-6", // Keeping classes for now, can be replaced by Mantine Icon component if available or styled with sx prop padding: "1.5rem",
color: "var(--mantine-color-dimmed)", // Set color via prop }}
})} >
</Group> <div className="flex items-center justify-between">
<Title order={3} fw={700} mt="xs"> <div className="flex-1">
{kpi.value} <h3
</Title> className="text-sm font-medium mb-1"
{kpi.delta && ( style={subtitleStyle}
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: "dimmed"
}
mt={4}
> >
{kpi.delta} {kpi.title}
</Text> </h3>
)} <p
{kpi.sub && ( className="text-3xl font-bold mb-1"
<Text size="xs" c="dimmed" mt={2}> style={textStyle}
{kpi.sub} >
</Text> {kpi.value}
)} </p>
</Card> <p
</Grid.Col> className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1E3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))} ))}
</Grid> </div>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> {/* Row 2: Full Width Weekly Bar Chart */}
<Title order={3} fw={500} mb="md"> <div
Interaksi Chatbot className="rounded-xl shadow-sm p-6 mb-6"
</Title> style={{
<BarChart ...cardStyle,
h={300} borderRadius: "12px",
data={chartData} boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
dataKey="day" padding: "1.5rem",
series={[{ name: 'total', color: 'blue' }]} marginBottom: "1.5rem",
withLegend }}
/> >
</Card> <h3
className="text-lg font-semibold mb-4"
{/* Charts and Lists Section */} style={textStyle}
<Grid gutter="lg"> >
{/* Grafik Interaksi Chatbot (now Bar Chart) */} Interaksi Chatbot Mingguan
<Grid.Col span={{ base: 12, lg: 6 }}> </h3>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> <ResponsiveContainer width="100%" height={300}>
<Title order={3} fw={500} mb="md"> <BarChart data={weeklyData}>
Jam Tersibuk <CartesianGrid
</Title> strokeDasharray="3 3"
<Stack gap="sm"> vertical={false}
{busyHours.map((item, index) => ( stroke={dark ? "#2d3748" : "#E5E7EB"}
<Box key={index}> />
<Text size="sm"> <XAxis
{item.period} dataKey="day"
</Text> axisLine={false}
<Group align="center"> tickLine={false}
<Progress value={item.percentage} flex={1} /> tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
<Text size="sm" fw={500}> />
{item.percentage}% <YAxis
</Text> axisLine={false}
</Group> tickLine={false}
</Box> tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="interactions" radius={[4, 4, 0, 0]}>
{weeklyData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))} ))}
</Stack> </Bar>
</Card> </BarChart>
</Grid.Col> </ResponsiveContainer>
</div>
{/* Topik Pertanyaan Terbanyak & Jam Tersibuk */} {/* Row 3: Two Insight Cards */}
<Grid.Col span={{ base: 12, lg: 6 }}> <div
<Stack gap="lg"> className="grid grid-cols-1 lg:grid-cols-2 gap-6"
{/* Topik Pertanyaan Terbanyak */} style={{
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> display: "grid",
<Title order={3} fw={500} mb="md"> gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
Topik Pertanyaan Terbanyak gap: "1.5rem",
</Title> }}
<Stack gap="xs"> >
{topTopics.map((item, index) => ( {/* Left: Frequently Asked Topics */}
<Group <div
key={index} className="rounded-xl shadow-sm p-6"
justify="space-between" style={{
align="center" ...cardStyle,
p="xs" borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Topik Pertanyaan Terbanyak
</h3>
<div className="space-y-3">
{topTopics.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-3"
style={{
borderBottom: `1px solid ${dark ? "#2d3748" : "#E5E7EB"}`,
}}
>
<span
className="text-sm font-medium"
style={textStyle}
>
{item.topic}
</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-darmasaba-blue-100 text-darmasaba-blue-800">
{item.count}x
</span>
</div>
))}
</div>
</div>
{/* Right: Busy Hour Distribution */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Distribusi Jam Tersibuk
</h3>
<div className="space-y-4">
{busyHours.map((item, index) => (
<div key={index}>
<div className="flex items-center justify-between mb-1">
<span
className="text-sm font-medium"
style={textStyle}
> >
<Text size="sm" fw={500}> {item.period}
{item.topic} </span>
</Text> <span
<Badge variant="light" color="gray"> className="text-sm font-semibold"
{item.count}x style={textStyle}
</Badge> >
</Group> {item.percentage}%
))} </span>
</Stack> </div>
</Card> <div
className="w-full rounded-full h-2"
{/* Jam Tersibuk */} style={{ backgroundColor: dark ? "#2d3748" : "#E5E7EB" }}
</Stack> >
</Grid.Col> <div
</Grid > className="h-2 rounded-full transition-all"
style={{
</Stack > width: `${item.percentage}%`,
</Box > backgroundColor: COLORS[index % COLORS.length],
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
); );
};
}
export default JennaAnalytic; export default JennaAnalytic;

View File

@@ -1,225 +1,318 @@
import { useState } from "react"; import {
import { Badge,
Card, Box,
Grid, Card,
GridCol, Grid,
Group, GridCol,
Text, Group,
Title, List,
Stack, Stack,
useMantineColorScheme, Text,
Badge, ThemeIcon,
List, Title,
ThemeIcon, useMantineColorScheme,
Box
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconCamera, IconAlertTriangle,
IconAlertTriangle, IconCamera,
IconMapPin, IconClock,
IconClock, IconEye,
IconEye, IconMapPin,
IconShieldLock IconShieldLock,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useState } from "react";
const KeamananPage = () => { const KeamananPage = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
// Sample data for KPI cards
const kpiData = [
{
title: "CCTV Aktif",
value: 20,
subtitle: "Kamera Online",
icon: <IconCamera size={24} />,
color: "darmasaba-success",
},
{
title: "Laporan Keamanan",
value: 15,
subtitle: "Minggu ini",
icon: <IconAlertTriangle size={24} />,
color: "darmasaba-danger",
},
];
// Sample data for CCTV locations // Sample data for KPI cards
const cctvLocations = [ const kpiData = [
{ id: "CCTV-01", lat: -8.5, lng: 115.2, status: "active", lastSeen: "2 jam yang lalu", location: "Balai Desa" }, {
{ id: "CCTV-02", lat: -8.6, lng: 115.3, status: "active", lastSeen: "1 jam yang lalu", location: "Pintu Masuk Desa" }, title: "CCTV Aktif",
{ id: "CCTV-03", lat: -8.4, lng: 115.1, status: "offline", lastSeen: "1 hari yang lalu", location: "Taman Desa" }, value: 20,
{ id: "CCTV-04", lat: -8.7, lng: 115.4, status: "active", lastSeen: "30 menit yang lalu", location: "Pasar Desa" }, subtitle: "Kamera Online",
]; icon: <IconCamera size={24} />,
color: "darmasaba-success",
},
{
title: "Laporan Keamanan",
value: 15,
subtitle: "Minggu ini",
icon: <IconAlertTriangle size={24} />,
color: "darmasaba-danger",
},
];
// Sample data for security reports // Sample data for CCTV locations
const securityReports = [ const cctvLocations = [
{ {
id: "REP-001", id: "CCTV-01",
title: "Pencurian Motor", lat: -8.5,
reportedAt: "2 jam yang lalu", lng: 115.2,
date: "12 Feb 2026, 14:30", status: "active",
location: "Jl. Kecubung 20", lastSeen: "2 jam yang lalu",
status: "baru", location: "Balai Desa",
}, },
{ {
id: "REP-002", id: "CCTV-02",
title: "Kerusuhan Antar Warga", lat: -8.6,
reportedAt: "4 jam yang lalu", lng: 115.3,
date: "12 Feb 2026, 12:15", status: "active",
location: "RT 05 RW 02", lastSeen: "1 jam yang lalu",
status: "baru", location: "Pintu Masuk Desa",
}, },
{ {
id: "REP-003", id: "CCTV-03",
title: "Kebakaran Rumah", lat: -8.4,
reportedAt: "1 hari yang lalu", lng: 115.1,
date: "11 Feb 2026, 08:45", status: "offline",
location: "Jl. Flamboyan 15", lastSeen: "1 hari yang lalu",
status: "diproses", location: "Taman Desa",
}, },
{ {
id: "REP-004", id: "CCTV-04",
title: "Kehilangan Barang", lat: -8.7,
reportedAt: "2 hari yang lalu", lng: 115.4,
date: "10 Feb 2026, 16:20", status: "active",
location: "Taman Desa", lastSeen: "30 menit yang lalu",
status: "selesai", location: "Pasar Desa",
}, },
]; ];
return ( // Sample data for security reports
<Stack gap="lg"> const securityReports = [
{/* Page Header */} {
<Group justify="space-between" align="center"> id: "REP-001",
<Title order={2} c={dark ? "dark.0" : "black"}> title: "Pencurian Motor",
Keamanan Lingkungan Desa reportedAt: "2 jam yang lalu",
</Title> date: "12 Feb 2026, 14:30",
</Group> location: "Jl. Kecubung 20",
status: "baru",
},
{
id: "REP-002",
title: "Kerusuhan Antar Warga",
reportedAt: "4 jam yang lalu",
date: "12 Feb 2026, 12:15",
location: "RT 05 RW 02",
status: "baru",
},
{
id: "REP-003",
title: "Kebakaran Rumah",
reportedAt: "1 hari yang lalu",
date: "11 Feb 2026, 08:45",
location: "Jl. Flamboyan 15",
status: "diproses",
},
{
id: "REP-004",
title: "Kehilangan Barang",
reportedAt: "2 hari yang lalu",
date: "10 Feb 2026, 16:20",
location: "Taman Desa",
status: "selesai",
},
];
{/* KPI Cards */} return (
<Grid gutter="md"> <Stack gap="lg">
{kpiData.map((kpi, index) => ( {/* KPI Cards */}
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}> <Grid gutter="md">
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> {kpiData.map((kpi, index) => (
<Group justify="space-between" align="center"> <GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
<Stack gap={0}> <Card
<Text size="sm" c={dark ? "dark.3" : "dimmed"}> p="md"
{kpi.subtitle} radius="md"
</Text> withBorder
<Group gap="xs" align="center"> bg={dark ? "#141D34" : "white"}
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}> style={{ borderColor: dark ? "#141D34" : "white" }}
{kpi.value} h="100%"
</Text> >
<Text size="sm" c={dark ? "dark.3" : "dimmed"}> <Group justify="space-between" align="center">
{kpi.title} <Stack gap={0}>
</Text> <Text size="sm" c={dark ? "dark.3" : "dimmed"}>
</Group> {kpi.subtitle}
</Stack> </Text>
<ThemeIcon variant="light" color={kpi.color} size="xl" radius="xl"> <Group gap="xs" align="center">
{kpi.icon} <Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
</ThemeIcon> {kpi.value}
</Group> </Text>
</Card> <Text size="sm" c={dark ? "dark.3" : "dimmed"}>
</GridCol> {kpi.title}
))} </Text>
</Grid> </Group>
</Stack>
<ThemeIcon
variant="light"
color={kpi.color}
size="xl"
radius="xl"
>
{kpi.icon}
</ThemeIcon>
</Group>
</Card>
</GridCol>
))}
</Grid>
<Grid gutter="md"> <Grid gutter="md">
{/* Peta Keamanan CCTV */} {/* Peta Keamanan CCTV */}
<GridCol span={{ base: 12, lg: 6 }}> <GridCol span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> <Card
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Peta Keamanan CCTV</Title> p="md"
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">Titik Lokasi CCTV</Text> radius="md"
withBorder
{/* Placeholder for map */} bg={dark ? "#141D34" : "white"}
<Box style={{ borderColor: dark ? "#141D34" : "white" }}
style={{ h="100%"
backgroundColor: dark ? '#2d3748' : '#e2e8f0', >
borderRadius: '8px', <Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
height: '400px', Peta Keamanan CCTV
display: 'flex', </Title>
alignItems: 'center', <Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">
justifyContent: 'center' Titik Lokasi CCTV
}} </Text>
>
<Stack align="center">
<IconMapPin size={48} stroke={1.5} color={dark ? '#94a3b8' : '#64748b'} />
<Text c={dark ? "dark.3" : "dimmed"}>Peta Lokasi CCTV</Text>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} ta="center">Integrasi dengan Google Maps atau Mapbox akan ditampilkan di sini</Text>
</Stack>
</Box>
{/* CCTV Locations List */}
<Stack mt="md" gap="sm">
<Title order={4} c={dark ? "dark.0" : "black"}>Daftar CCTV</Title>
{cctvLocations.map((cctv, index) => (
<Card key={index} p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}>
<Group justify="space-between">
<Stack gap={0}>
<Group gap="xs">
<Text fw={500} c={dark ? "dark.0" : "black"}>{cctv.id}</Text>
<Badge
variant="dot"
color={cctv.status === "active" ? "green" : "gray"}
>
{cctv.status === "active" ? "Online" : "Offline"}
</Badge>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{cctv.location}</Text>
</Stack>
<Group gap="xs">
<IconClock size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{cctv.lastSeen}</Text>
</Group>
</Group>
</Card>
))}
</Stack>
</Card>
</GridCol>
{/* Daftar Laporan Keamanan */} {/* Placeholder for map */}
<GridCol span={{ base: 12, lg: 6 }}> <Box
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> style={{
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Laporan Keamanan Lingkungan</Title> backgroundColor: dark ? "#2d3748" : "#e2e8f0",
borderRadius: "8px",
<Stack gap="sm"> height: "400px",
{securityReports.map((report, index) => ( display: "flex",
<Card key={index} p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}> alignItems: "center",
<Group justify="space-between" mb="sm"> justifyContent: "center",
<Text fw={500} c={dark ? "dark.0" : "black"}>{report.title}</Text> }}
<Badge >
variant="light" <Stack align="center">
color={ <IconMapPin
report.status === "baru" ? "red" : size={48}
report.status === "diproses" ? "yellow" : "green" stroke={1.5}
} color={dark ? "#94a3b8" : "#64748b"}
> />
{report.status} <Text c={dark ? "dark.3" : "dimmed"}>Peta Lokasi CCTV</Text>
</Badge> <Text size="sm" c={dark ? "dark.3" : "dimmed"} ta="center">
</Group> Integrasi dengan Google Maps atau Mapbox akan ditampilkan di
sini
<Group justify="space-between"> </Text>
<Group gap="xs"> </Stack>
<IconMapPin size={16} stroke={1.5} /> </Box>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{report.location}</Text>
</Group> {/* CCTV Locations List */}
<Group gap="xs"> <Stack mt="md" gap="sm">
<IconClock size={16} stroke={1.5} /> <Title order={4} c={dark ? "dark.0" : "black"}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{report.reportedAt}</Text> Daftar CCTV
</Group> </Title>
</Group> {cctvLocations.map((cctv, index) => (
<Card
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mt="sm">{report.date}</Text> key={index}
</Card> p="md"
))} radius="md"
</Stack> withBorder
</Card> bg={dark ? "#263852ff" : "#F1F5F9"}
</GridCol> style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
</Grid> >
</Stack> <Group justify="space-between">
); <Stack gap={0}>
<Group gap="xs">
<Text fw={500} c={dark ? "dark.0" : "black"}>
{cctv.id}
</Text>
<Badge
variant="dot"
color={cctv.status === "active" ? "green" : "gray"}
>
{cctv.status === "active" ? "Online" : "Offline"}
</Badge>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{cctv.location}
</Text>
</Stack>
<Group gap="xs">
<IconClock size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{cctv.lastSeen}
</Text>
</Group>
</Group>
</Card>
))}
</Stack>
</Card>
</GridCol>
{/* Daftar Laporan Keamanan */}
<GridCol span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="md"
withBorder
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
h="100%"
>
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
Laporan Keamanan Lingkungan
</Title>
<Stack gap="sm">
{securityReports.map((report, index) => (
<Card
key={index}
p="md"
radius="md"
withBorder
bg={dark ? "#263852ff" : "#F1F5F9"}
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
>
<Group justify="space-between" mb="sm">
<Text fw={500} c={dark ? "dark.0" : "black"}>
{report.title}
</Text>
<Badge
variant="light"
color={
report.status === "baru"
? "red"
: report.status === "diproses"
? "yellow"
: "green"
}
>
{report.status}
</Badge>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconMapPin size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{report.location}
</Text>
</Group>
<Group gap="xs">
<IconClock size={16} stroke={1.5} />
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
{report.reportedAt}
</Text>
</Group>
</Group>
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mt="sm">
{report.date}
</Text>
</Card>
))}
</Stack>
</Card>
</GridCol>
</Grid>
</Stack>
);
}; };
export default KeamananPage; export default KeamananPage;

View File

@@ -1,307 +1,568 @@
import React from "react";
import { import {
Button, IconCurrency,
Card, IconTrendingDown,
Badge, IconTrendingUp,
Title, IconCheck,
Text, IconClock,
Group, } from "@tabler/icons-react";
Stack, import { useMantineColorScheme } from "@mantine/core";
Grid, import {
Box, Bar,
Progress, BarChart,
useMantineColorScheme, CartesianGrid,
} from "@mantine/core"; Cell,
import { IconTrendingUp, IconTrendingDown, IconCurrency } from "@tabler/icons-react"; Line,
import { BarChart } from "@mantine/charts"; LineChart,
ResponsiveContainer,
// Sample Data Tooltip,
const kpiData = [ XAxis,
{ YAxis,
id: 1, } from "recharts";
title: "Total APBDes",
value: "Rp 5.2M",
sub: "Tahun 2025",
icon: (
<IconCurrency className="h-6 w-6 text-muted-foreground" />
),
},
{
id: 2,
title: "Realisasi",
value: "68%",
sub: "Rp 3.5M dari 5.2M",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
/>
</svg>
),
},
{
id: 3,
title: "Pemasukan",
value: "Rp 580jt",
sub: "Bulan ini",
delta: "+8%",
deltaType: "positive",
icon: (
<IconTrendingUp className="h-6 w-6 text-muted-foreground" />
),
},
{
id: 4,
title: "Pengeluaran",
value: "Rp 520jt",
sub: "Bulan ini",
icon: (
<IconTrendingDown className="h-6 w-6 text-muted-foreground" />
),
},
];
const incomeExpenseData = [
{ month: "Apr", income: 450, expense: 380 },
{ month: "Mei", income: 520, expense: 420 },
{ month: "Jun", income: 480, expense: 500 },
{ month: "Jul", income: 580, expense: 450 },
{ month: "Agu", income: 550, expense: 520 },
{ month: "Sep", income: 600, expense: 480 },
{ month: "Okt", income: 580, expense: 520 },
];
const allocationData = [
{ sector: "Pembangunan", amount: 1200 },
{ sector: "Kesehatan", amount: 800 },
{ sector: "Pendidikan", amount: 650 },
{ sector: "Sosial", amount: 550 },
{ sector: "Kebudayaan", amount: 400 },
{ sector: "Teknologi", amount: 300 },
];
const assistanceFundData = [
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
];
const apbdReport = {
income: [
{ category: "Dana Desa", amount: 1800 },
{ category: "Alokasi Dana Desa", amount: 480 },
{ category: "Bagi Hasil Pajak & Retribusi", amount: 300 },
{ category: "Pendapatan Asli Desa", amount: 200 },
{ category: "Hibah Bantuan", amount: 300 },
],
expenses: [
{ category: "Penyelenggaraan Pemerintah", amount: 425 },
{ category: "Pembangunan Desa", amount: 850 },
{ category: "Pembinaan Kemasyarakatan", amount: 320 },
{ category: "Pemberdayaan Masyarakat", amount: 380 },
{ category: "Penanggulangan Bencana", amount: 180 },
],
totalIncome: 3080,
totalExpenses: 2155,
};
const KeuanganAnggaran = () => { const KeuanganAnggaran = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
// KPI Data
const kpiData = [
{
id: 1,
title: "Total APBDes",
value: "Rp 5.2M",
subtitle: "Tahun 2025",
icon: IconCurrency,
},
{
id: 2,
title: "Realisasi",
value: "68%",
subtitle: "Rp 3.5M dari 5.2M",
icon: IconCheck,
},
{
id: 3,
title: "Pemasukan",
value: "Rp 580jt",
subtitle: "Bulan ini",
delta: "+8%",
icon: IconTrendingUp,
},
{
id: 4,
title: "Pengeluaran",
value: "Rp 520jt",
subtitle: "Bulan ini",
icon: IconTrendingDown,
},
];
// Income vs Expense data
const incomeExpenseData = [
{ month: "Apr", income: 450, expense: 380 },
{ month: "Mei", income: 520, expense: 420 },
{ month: "Jun", income: 480, expense: 500 },
{ month: "Jul", income: 580, expense: 450 },
{ month: "Agu", income: 550, expense: 520 },
{ month: "Sep", income: 600, expense: 480 },
{ month: "Okt", income: 580, expense: 520 },
];
// Allocation data
const allocationData = [
{ sector: "Pembangunan", amount: 1200 },
{ sector: "Kesehatan", amount: 800 },
{ sector: "Pendidikan", amount: 650 },
{ sector: "Sosial", amount: 550 },
{ sector: "Kebudayaan", amount: 400 },
{ sector: "Teknologi", amount: 300 },
];
// Assistance fund data
const assistanceFundData = [
{ source: "Dana Desa (DD)", amount: 1800, status: "cair" },
{ source: "Alokasi Dana Desa (ADD)", amount: 950, status: "cair" },
{ source: "Bagi Hasil Pajak", amount: 450, status: "cair" },
{ source: "Hibah Provinsi", amount: 300, status: "proses" },
];
// APBDes Report data
const apbdReport = {
income: [
{ category: "Dana Desa", amount: 1800 },
{ category: "Alokasi Dana Desa", amount: 480 },
{ category: "Bagi Hasil Pajak & Retribusi", amount: 300 },
{ category: "Pendapatan Asli Desa", amount: 200 },
{ category: "Hibah Bantuan", amount: 300 },
],
expenses: [
{ category: "Penyelenggaraan Pemerintah", amount: 425 },
{ category: "Pembangunan Desa", amount: 850 },
{ category: "Pembinaan Kemasyarakatan", amount: 320 },
{ category: "Pemberdayaan Masyarakat", amount: 380 },
{ category: "Penanggulangan Bencana", amount: 180 },
],
totalIncome: 3080,
totalExpenses: 2155,
};
const COLORS = ["#1E3A5F", "#3B82F6", "#60A5FA", "#93C5FD", "#DBEAFE"];
const cardStyle = {
backgroundColor: dark ? "#1E293B" : "white",
border: `1px solid ${dark ? "#1E293B" : "white"}`,
};
const textStyle = {
color: dark ? "white" : "#1F2937",
};
const subtitleStyle = {
color: dark ? "#9CA3AF" : "#6B7280",
};
return ( return (
<Box> <div
<Stack gap="xl"> className="min-h-screen"
{/* KPI Cards */} style={{
<Grid gutter="lg"> backgroundColor: dark ? "#0F172A" : "#F3F4F6",
minHeight: "100vh",
padding: "1.5rem",
}}
>
<div
className="max-w-7xl mx-auto"
style={{
maxWidth: "80rem",
marginLeft: "auto",
marginRight: "auto",
}}
>
{/* Row 1: 4 Summary Metrics Cards */}
<div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{kpiData.map((kpi) => ( {kpiData.map((kpi) => (
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}> <div
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> key={kpi.id}
<Group justify="space-between" align="flex-start" mb="xs"> className="rounded-xl shadow-sm p-6"
<Text size="sm" fw={500} c="dimmed"> style={{
{kpi.title} ...cardStyle,
</Text> borderRadius: "12px",
{React.cloneElement(kpi.icon, { boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
className: "h-6 w-6", padding: "1.5rem",
color: "var(--mantine-color-dimmed)", }}
})} >
</Group> <div className="flex items-center justify-between">
<Title order={3} fw={700} mt="xs"> <div className="flex-1">
{kpi.value} <h3
</Title> className="text-sm font-medium mb-1"
{kpi.delta && ( style={subtitleStyle}
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: "dimmed"
}
mt={4}
> >
{kpi.delta} {kpi.title}
</Text> </h3>
)} <p
{kpi.sub && ( className="text-3xl font-bold mb-1"
<Text size="xs" c="dimmed" mt="auto"> style={textStyle}
{kpi.sub} >
</Text> {kpi.value}
)} </p>
</Card> <p
</Grid.Col> className="text-xs"
style={subtitleStyle}
>
{kpi.subtitle}
</p>
{kpi.delta && (
<p className="text-xs mt-1" style={{ color: "#22C55E" }}>
{kpi.delta}
</p>
)}
</div>
<div className="flex-shrink-0 ml-4">
<div
className="w-12 h-12 rounded-full flex items-center justify-center text-white"
style={{ backgroundColor: "#1F3A5F" }}
>
<kpi.icon size={24} />
</div>
</div>
</div>
</div>
))} ))}
</Grid> </div>
{/* Charts Section */} {/* Row 2: Line Chart Section */}
<Grid gutter="lg"> <div
{/* Grafik Pemasukan vs Pengeluaran */} className="rounded-xl shadow-sm p-6 mb-6"
<Grid.Col span={{ base: 12, lg: 6 }}> style={{
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> ...cardStyle,
<Title order={3} fw={500} mb="md"> borderRadius: "12px",
Pemasukan vs Pengeluaran boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
</Title> padding: "1.5rem",
<BarChart marginBottom: "1.5rem",
h={300} }}
data={incomeExpenseData} >
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Pemasukan vs Pengeluaran
</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={incomeExpenseData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#E5E7EB"}
/>
<XAxis
dataKey="month" dataKey="month"
series={[ axisLine={false}
{ name: 'income', color: 'green', label: 'Pemasukan' }, tickLine={false}
{ name: 'expense', color: 'red', label: 'Pengeluaran' }, tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
]}
withLegend
/> />
</Card> <YAxis
</Grid.Col> axisLine={false}
tickLine={false}
{/* Alokasi Anggaran Per Sektor */} tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
<Grid.Col span={{ base: 12, lg: 6 }}> tickFormatter={(value) => `${value}jt`}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={3} fw={500} mb="md">
Alokasi Anggaran Per Sektor
</Title>
<BarChart
h={300}
data={allocationData}
dataKey="sector"
series={[{ name: 'amount', color: 'darmasaba-navy', label: 'Jumlah' }]}
withLegend
orientation="horizontal"
/> />
</Card> <Tooltip
</Grid.Col> contentStyle={{
</Grid> backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Line
type="monotone"
dataKey="income"
stroke="#22C55E"
strokeWidth={3}
dot={{ fill: "#22C55E", strokeWidth: 2, r: 5 }}
activeDot={{ r: 7 }}
name="Pemasukan"
/>
<Line
type="monotone"
dataKey="expense"
stroke="#EF4444"
strokeWidth={3}
dot={{ fill: "#EF4444", strokeWidth: 2, r: 5 }}
activeDot={{ r: 7 }}
name="Pengeluaran"
/>
</LineChart>
</ResponsiveContainer>
<Grid gutter="lg"> {/* Legend */}
{/* Dana Bantuan & Hibah */} <div className="flex items-center gap-6 mt-4">
<Grid.Col span={{ base: 12, lg: 6 }}> <div className="flex items-center gap-2">
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div
<Title order={3} fw={500} mb="md"> className="w-3 h-3 rounded-full"
Dana Bantuan & Hibah style={{ backgroundColor: "#22C55E" }}
</Title> />
<Stack gap="sm"> <span className="text-sm" style={subtitleStyle}>
{assistanceFundData.map((fund, index) => ( Pemasukan
<Group </span>
key={index} </div>
justify="space-between" <div className="flex items-center gap-2">
align="center" <div
p="sm" className="w-3 h-3 rounded-full"
style={{ backgroundColor: "#EF4444" }}
/>
<span className="text-sm" style={subtitleStyle}>
Pengeluaran
</span>
</div>
</div>
</div>
{/* Row 3: Analytics Section */}
<div
className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "1.5rem",
marginBottom: "1.5rem",
}}
>
{/* Left: Horizontal Bar Chart */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Alokasi Anggaran Per Sektor
</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={allocationData} layout="vertical">
<CartesianGrid
strokeDasharray="3 3"
horizontal={false}
stroke={dark ? "#334155" : "#E5E7EB"}
/>
<XAxis
type="number"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
tickFormatter={(value) => `${value}jt`}
/>
<YAxis
dataKey="sector"
type="category"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#374151" }}
width={120}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="amount" radius={[0, 4, 4, 0]}>
{allocationData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={COLORS[index % COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Right: Assistance Funds List */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={textStyle}
>
Dana Bantuan dan Hibah
</h3>
<div className="space-y-3">
{assistanceFundData.map((fund, index) => (
<div
key={index}
className="flex items-center justify-between p-4 rounded-lg"
style={{
backgroundColor: dark ? "#334155" : "#F9FAFB",
}}
>
<div>
<p
className="text-sm font-medium"
style={textStyle}
>
{fund.source}
</p>
<p
className="text-xs mt-1"
style={subtitleStyle}
>
Rp {fund.amount.toLocaleString()}jt
</p>
</div>
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium"
style={{ style={{
border: "1px solid var(--mantine-color-gray-3)", backgroundColor: fund.status === "cair" ? "#DCFCE7" : "#FEF3C7",
borderRadius: "var(--mantine-radius-sm)", color: fund.status === "cair" ? "#166534" : "#92400E",
}} }}
> >
<Box> {fund.status === "cair" ? (
<Text size="sm" fw={500}> <IconCheck size={14} className="mr-1" />
{fund.source} ) : (
</Text> <IconClock size={14} className="mr-1" />
<Text size="sm" c="dimmed"> )}
Rp {fund.amount.toLocaleString()}jt {fund.status}
</Text> </span>
</Box> </div>
<Badge ))}
variant="light" </div>
color={fund.status === "cair" ? "green" : "yellow"} </div>
</div>
{/* Row 4: Report Section */}
<div
className="rounded-xl shadow-sm p-6"
style={{
...cardStyle,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.5rem",
}}
>
<h3
className="text-lg font-semibold mb-6"
style={textStyle}
>
Laporan APBDes
</h3>
<div
className="grid grid-cols-1 md:grid-cols-2 gap-8"
style={{
display: "grid",
gridTemplateColumns: "repeat(2, 1fr)",
gap: "2rem",
}}
>
{/* Left: Pendapatan */}
<div>
<h4
className="text-base font-semibold mb-4"
style={textStyle}
>
Pendapatan
</h4>
<div className="space-y-3">
{apbdReport.income.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-2"
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
<span className="text-sm" style={subtitleStyle}>
{item.category}
</span>
<span
className="text-sm font-medium"
style={{ color: "#22C55E" }}
> >
{fund.status} Rp {item.amount.toLocaleString()}jt
</Badge> </span>
</Group> </div>
))} ))}
</Stack> <div
</Card> className="flex items-center justify-between py-3 mt-4 pt-4"
</Grid.Col> style={{
borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<span className="text-base font-bold" style={textStyle}>
Total Pendapatan
</span>
<span
className="text-base font-bold"
style={{ color: "#22C55E" }}
>
Rp {apbdReport.totalIncome.toLocaleString()}jt
</span>
</div>
</div>
</div>
{/* Laporan APBDes */} {/* Right: Belanja */}
<Grid.Col span={{ base: 12, lg: 6 }}> <div>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <h4
<Title order={3} fw={500} mb="md"> className="text-base font-semibold mb-4"
Laporan APBDes style={textStyle}
</Title> >
Belanja
</h4>
<div className="space-y-3">
{apbdReport.expenses.map((item, index) => (
<div
key={index}
className="flex items-center justify-between py-2"
style={{
borderBottom: `1px solid ${dark ? "#334155" : "#F3F4F6"}`,
}}
>
<span className="text-sm" style={subtitleStyle}>
{item.category}
</span>
<span
className="text-sm font-medium"
style={{ color: "#EF4444" }}
>
Rp {item.amount.toLocaleString()}jt
</span>
</div>
))}
<div
className="flex items-center justify-between py-3 mt-4 pt-4"
style={{
borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
}}
>
<span className="text-base font-bold" style={textStyle}>
Total Belanja
</span>
<span
className="text-base font-bold"
style={{ color: "#EF4444" }}
>
Rp {apbdReport.totalExpenses.toLocaleString()}jt
</span>
</div>
</div>
</div>
</div>
<Box mb="md"> {/* Footer: Balance */}
<Title order={4} mb="sm">Pendapatan</Title> <div
<Stack gap="xs"> className="flex items-center justify-between py-4 mt-6 pt-6"
{apbdReport.income.map((item, index) => ( style={{
<Group key={index} justify="space-between"> borderTop: `2px solid ${dark ? "#334155" : "#E5E7EB"}`,
<Text size="sm">{item.category}</Text> }}
<Text size="sm" c="green"> >
Rp {item.amount.toLocaleString()}jt <span className="text-lg font-bold" style={textStyle}>
</Text> Saldo
</Group> </span>
))} <span
<Group justify="space-between" mt="sm"> className="text-lg font-bold"
<Text fw={700}>Total Pendapatan:</Text> style={{
<Text fw={700} c="green"> color:
Rp {apbdReport.totalIncome.toLocaleString()}jt apbdReport.totalIncome > apbdReport.totalExpenses
</Text> ? "#22C55E"
</Group> : "#EF4444",
</Stack> }}
</Box> >
Rp{" "}
{(
apbdReport.totalIncome - apbdReport.totalExpenses
).toLocaleString()}
jt
</span>
</div>
</div>
<Box> </div>
<Title order={4} mb="sm">Belanja</Title> </div>
<Stack gap="xs">
{apbdReport.expenses.map((item, index) => (
<Group key={index} justify="space-between">
<Text size="sm">{item.category}</Text>
<Text size="sm" c="red">
Rp {item.amount.toLocaleString()}jt
</Text>
</Group>
))}
<Group justify="space-between" mt="sm">
<Text fw={700}>Total Belanja:</Text>
<Text fw={700} c="red">
Rp {apbdReport.totalExpenses.toLocaleString()}jt
</Text>
</Group>
</Stack>
</Box>
<Box mt="md" pt="md" style={{ borderTop: '1px solid var(--mantine-color-gray-3)' }}>
<Group justify="space-between">
<Text fw={700}>Saldo:</Text>
<Text fw={700} c={apbdReport.totalIncome > apbdReport.totalExpenses ? "green" : "red"}>
Rp {(apbdReport.totalIncome - apbdReport.totalExpenses).toLocaleString()}jt
</Text>
</Group>
</Box>
</Card>
</Grid.Col>
</Grid>
</Stack>
</Box>
); );
}; };
export default KeuanganAnggaran; export default KeuanganAnggaran;

View File

@@ -1,343 +1,390 @@
import { import {
Stack, Bar,
Grid, BarChart,
GridCol, CartesianGrid,
Group, Cell,
Text, Pie,
Title, PieChart,
ActionIcon, ResponsiveContainer,
Progress as MantineProgress, Tooltip,
Box, XAxis,
Badge as MantineBadge, YAxis,
Card, } from "recharts";
useMantineColorScheme, import { useMantineColorScheme } from "@mantine/core";
ThemeIcon, import { IconMessage } from "@tabler/icons-react";
List,
Divider,
Skeleton
} from "@mantine/core";
import { Button } from "@/components/ui/button";
import { Bar, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from "recharts";
const KinerjaDivisi = () => { const KinerjaDivisi = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
// Data for division progress chart // Top row - 4 activity cards
const divisionProgressData = [ const activities = [
{ name: "Sekretariat", selesai: 12, berjalan: 5, tertunda: 2 },
{ name: "Keuangan", selesai: 8, berjalan: 7, tertunda: 1 },
{ name: "Sosial", selesai: 10, berjalan: 3, tertunda: 4 },
{ name: "Humas", selesai: 6, berjalan: 9, tertunda: 3 },
];
// Division task summaries
const divisionTasks = [
{ {
name: "Sekretariat", title: "Rakor 2025",
tasks: [ progress: 100,
{ title: "Laporan Bulanan", status: "selesai" }, date: "15 Jan 2025",
{ title: "Arsip Dokumen", status: "berjalan" },
{ title: "Undangan Rapat", status: "tertunda" },
]
}, },
{ {
name: "Keuangan", title: "Pemutakhiran Indeks Desa",
tasks: [ progress: 100,
{ title: "Laporan APBDes", status: "selesai" }, date: "20 Feb 2025",
{ title: "Verifikasi Dana", status: "tertunda" },
{ title: "Pengeluaran Harian", status: "berjalan" },
]
}, },
{ {
name: "Sosial", title: "Mengurus akta cerai warga",
tasks: [ progress: 100,
{ title: "Program Bantuan", status: "selesai" }, date: "5 Mar 2025",
{ title: "Kegiatan Posyandu", status: "berjalan" },
{ title: "Monitoring Stunting", status: "tertunda" },
]
}, },
{ {
name: "Humas", title: "Pasek 7 desa adat",
tasks: [ progress: 100,
{ title: "Publikasi Kegiatan", status: "selesai" }, date: "10 Mar 2025",
{ title: "Koordinasi Media", status: "berjalan" },
{ title: "Laporan Kegiatan", status: "tertunda" },
]
}, },
]; ];
// Archive items
const archiveItems = [
{ name: "Surat Keputusan", count: 12 },
{ name: "Laporan Keuangan", count: 8 },
{ name: "Dokumentasi", count: 24 },
{ name: "Notulensi Rapat", count: 15 },
];
// Activity progress
const activityProgress = [
{ name: "Pembangunan Jalan", progress: 75, date: "15 Feb 2026", status: "berjalan" },
{ name: "Posyandu Bulanan", progress: 100, date: "10 Feb 2026", status: "selesai" },
{ name: "Vaksinasi Massal", progress: 45, date: "20 Feb 2026", status: "berjalan" },
{ name: "Festival Budaya", progress: 20, date: "5 Mar 2026", status: "berjalan" },
];
// Document statistics // Document statistics
const documentStats = [ const documentStats = [
{ name: "Gambar", value: 42 }, { name: "Gambar", value: 300, color: "#FAC858" },
{ name: "Dokumen", value: 87 }, { name: "Dokumen", value: 310, color: "#92CC76" },
]; ];
// Activity progress statistics // Activity progress statistics
const activityProgressStats = [ const activityProgressStats = [
{ name: "Selesai", value: 12 }, { name: "Selesai", value: 83.33, fill: "#92CC76" },
{ name: "Dikerjakan", value: 8 }, { name: "Dikerjakan", value: 16.67, fill: "#FAC858" },
{ name: "Segera Dikerjakan", value: 5 }, { name: "Segera Dikerjakan", value: 0, fill: "#5470C6" },
{ name: "Dibatalkan", value: 2 }, { name: "Dibatalkan", value: 0, fill: "#EE6767" },
]; ];
const COLORS = ['#10B981', '#F59E0B', '#EF4444', '#6B7280'];
const STATUS_COLORS: Record<string, string> = {
selesai: 'green',
berjalan: 'blue',
tertunda: 'red',
proses: 'yellow'
};
// Discussion data // Discussion data
const discussions = [ const discussions = [
{ title: "Pembahasan APBDes 2026", sender: "Kepala Desa", timestamp: "2 jam yang lalu" }, {
{ title: "Kegiatan Posyandu", sender: "Divisi Sosial", timestamp: "5 jam yang lalu" }, title: "Pembahasan APBDes 2026",
{ title: "Festival Budaya", sender: "Divisi Humas", timestamp: "1 hari yang lalu" }, sender: "Kepala Desa",
]; date: "10 Mar 2025",
},
// Today's agenda {
const todayAgenda = [ title: "Kegiatan Posyandu",
{ time: "09:00", event: "Rapat Evaluasi Bulanan" }, sender: "Divisi Sosial",
{ time: "14:00", event: "Koordinasi Program Bantuan" }, date: "9 Mar 2025",
},
{
title: "Festival Budaya",
sender: "Divisi Humas",
date: "8 Mar 2025",
},
]; ];
return ( return (
<Stack gap="lg"> <div
{/* Grafik Progres Tugas per Divisi */} className="min-h-screen"
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} > style={{
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}> backgroundColor: dark ? "#10192D" : "#F3F4F6",
Grafik Progres Tugas per Divisi minHeight: "100vh",
</Title> padding: "1.5rem",
<ResponsiveContainer width="100%" height={300}> }}
<BarChart data={divisionProgressData}> >
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={dark ? "#141D34" : "white"} /> {/* Top Row - 4 Activity Cards */}
<XAxis <div
dataKey="name" className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"
axisLine={false} style={{
tickLine={false} display: "grid",
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }} gridTemplateColumns: "repeat(4, 1fr)",
/> gap: "1.5rem",
<YAxis marginBottom: "1.5rem",
axisLine={false} }}
tickLine={false} >
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }} {activities.map((activity, index) => (
/> <div
<Tooltip key={index}
contentStyle={dark className="rounded-xl shadow-sm p-5"
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' } style={{
: {}} backgroundColor: dark ? "#141D34" : "white",
/> border: `1px solid ${dark ? "#141D34" : "white"}`,
<Bar dataKey="selesai" stackId="a" fill="#10B981" name="Selesai" radius={[4, 4, 0, 0]} /> borderRadius: "12px",
<Bar dataKey="berjalan" stackId="a" fill="#3B82F6" name="Berjalan" radius={[4, 4, 0, 0]} /> boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
<Bar dataKey="tertunda" stackId="a" fill="#EF4444" name="Tertunda" radius={[4, 4, 0, 0]} /> padding: "1.25rem",
</BarChart> }}
</ResponsiveContainer> >
</Card> {/* Dark blue title bar */}
<div
className="text-white px-3 py-2 rounded-t-lg -mx-5 -mt-5 mb-4"
style={{ backgroundColor: "#1E3A5F" }}
>
<h3 className="text-sm font-semibold">{activity.title}</h3>
</div>
{/* Ringkasan Tugas per Divisi */} {/* Orange progress bar */}
<Grid gutter="md"> <div
{divisionTasks.map((division, index) => ( className="w-full rounded-full h-2 mb-3"
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}> style={{ backgroundColor: dark ? "#2d3748" : "#E5E7EB" }}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> >
<Title order={4} mb="sm" c={dark ? 'white' : 'darmasaba-navy'}> <div
{division.name} className="bg-orange-500 h-2 rounded-full"
</Title> style={{ width: `${activity.progress}%` }}
<Stack gap="sm"> />
{division.tasks.map((task, taskIndex) => ( </div>
<Box key={taskIndex}>
<Group justify="space-between"> {/* Date and badge */}
<Text size="sm" c={dark ? 'white' : 'darmasaba-navy'}>{task.title}</Text> <div className="flex justify-between items-center">
<MantineBadge <span
color={STATUS_COLORS[task.status] || 'gray'} className="text-xs"
variant="light" style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
> >
{task.status} {activity.date}
</MantineBadge> </span>
</Group> <span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full font-medium">
</Box> Selesai
))} </span>
</Stack> </div>
</Card> </div>
</GridCol>
))} ))}
</Grid> </div>
{/* Arsip Digital Perangkat Desa */} {/* Second Row - Charts */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}> className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"
Arsip Digital Perangkat Desa style={{
</Title> display: "grid",
<Grid gutter="md"> gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
{archiveItems.map((item, index) => ( gap: "1.5rem",
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}> marginBottom: "1.5rem",
<Card p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}> }}
<Group justify="space-between"> >
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={500}>{item.name}</Text> {/* Left Card - Jumlah Dokumen (Bar Chart) */}
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={700}>{item.count}</Text> <div
</Group> className="rounded-xl shadow-sm p-5"
</Card> style={{
</GridCol> backgroundColor: dark ? "#141D34" : "white",
))} border: `1px solid ${dark ? "#141D34" : "white"}`,
</Grid> borderRadius: "12px",
</Card> boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.25rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Jumlah Dokumen
</h3>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={documentStats}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#2d3748" : "#E5E7EB"}
/>
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "#9CA3AF" : "#6B7280" }}
/>
<Tooltip
contentStyle={{
backgroundColor: dark ? "#1F2937" : "white",
border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
borderRadius: "8px",
color: dark ? "white" : "#1F2937",
}}
/>
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
{documentStats.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
{/* Kartu Progres Kegiatan */} {/* Right Card - Progres Kegiatan (Pie Chart) */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}> className="rounded-xl shadow-sm p-5"
Progres Kegiatan / Program style={{
</Title> backgroundColor: dark ? "#141D34" : "white",
<Stack gap="md"> border: `1px solid ${dark ? "#141D34" : "white"}`,
{activityProgress.map((activity, index) => ( borderRadius: "12px",
<Card key={index} p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}> boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
<Group justify="space-between" mb="sm"> padding: "1.25rem",
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={500}>{activity.name}</Text> }}
<MantineBadge >
color={STATUS_COLORS[activity.status] || 'gray'} <h3
variant="light" className="text-lg font-semibold mb-4"
> style={{ color: dark ? "white" : "#1F2937" }}
{activity.status} >
</MantineBadge> Progres Kegiatan
</Group> </h3>
<Group justify="space-between"> <ResponsiveContainer width="100%" height={250}>
<MantineProgress <PieChart>
value={activity.progress} <Pie
size="sm" data={activityProgressStats.filter(item => item.value > 0)}
radius="xl" cx="50%"
color={activity.progress === 100 ? "green" : "blue"} cy="50%"
w="calc(100% - 80px)" outerRadius={80}
/> dataKey="value"
<Text size="sm" c={dark ? 'white' : 'darmasaba-navy'}>{activity.progress}%</Text> label={({ name, percent }) =>
</Group> `${name}: ${percent ? (percent * 100).toFixed(0) : 0}%`
<Text size="sm" c="dimmed" mt="sm">{activity.date}</Text> }
</Card> >
))} {activityProgressStats
</Stack> .filter(item => item.value > 0)
</Card> .map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
{/* Statistik Dokumen & Progres Kegiatan */}
<Grid gutter="md">
<GridCol span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Jumlah Dokumen
</Title>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={documentStats}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={dark ? "#141D34" : "white"} />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
/>
<Tooltip
contentStyle={dark
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' }
: {}}
/>
<Bar dataKey="value" fill={dark ? "var(--mantine-color-blue-6)" : "var(--mantine-color-blue-filled)"} radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Card>
</GridCol>
<GridCol span={{ base: 12, lg: 6 }}>
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}>
Progres Kegiatan
</Title>
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie
data={activityProgressStats}
cx="50%"
cy="50%"
labelLine={false}
outerRadius={80}
fill="#8884d8"
dataKey="value"
label={({ name, percent }) => `${name}: ${percent ? (percent * 100).toFixed(0) : '0'}%`}
>
{activityProgressStats.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))} ))}
</Pie> </Pie>
<Tooltip <Tooltip
contentStyle={dark contentStyle={{
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' } backgroundColor: dark ? "#1F2937" : "white",
: {}} border: `1px solid ${dark ? "#374151" : "#E5E7EB"}`,
/> borderRadius: "8px",
</PieChart> color: dark ? "white" : "#1F2937",
</ResponsiveContainer> }}
</Card> />
</GridCol> </PieChart>
</Grid> </ResponsiveContainer>
{/* Diskusi Internal */} {/* Legend */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div className="mt-4 space-y-2">
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}> <div className="flex items-center gap-2">
Diskusi Internal <div className="w-3 h-3 rounded-full bg-blue-500"></div>
</Title> <span
<Stack gap="sm"> className="text-sm"
{discussions.map((discussion, index) => ( style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
<Card key={index} p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}> >
<Group justify="space-between"> Segera Dikerjakan
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={500}>{discussion.title}</Text> </span>
<Text size="sm" c="dimmed">{discussion.timestamp}</Text> </div>
</Group> <div className="flex items-center gap-2">
<Text size="sm" c="dimmed">{discussion.sender}</Text> <div className="w-3 h-3 rounded-full bg-yellow-500"></div>
</Card> <span
))} className="text-sm"
</Stack> style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
</Card> >
Dikerjakan
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Selesai
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500"></div>
<span
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#4B5563" }}
>
Dibatalkan
</span>
</div>
</div>
</div>
</div>
{/* Agenda / Acara Hari Ini */} {/* Bottom Row */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div
<Title order={4} mb="md" c={dark ? 'white' : 'darmasaba-navy'}> className="grid grid-cols-1 lg:grid-cols-2 gap-6"
Agenda / Acara Hari Ini style={{
</Title> display: "grid",
{todayAgenda.length > 0 ? ( gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
<Stack gap="sm"> gap: "1.5rem",
{todayAgenda.map((agenda, index) => ( }}
<Group key={index} align="flex-start"> >
<Box w={60}> {/* Left Card - Diskusi */}
<Text c="dimmed">{agenda.time}</Text> <div
</Box> className="rounded-xl shadow-sm p-5"
<Divider orientation="vertical" mx="sm" /> style={{
<Text c={dark ? 'white' : 'darmasaba-navy'}>{agenda.event}</Text> backgroundColor: dark ? "#141D34" : "white",
</Group> border: `1px solid ${dark ? "#141D34" : "white"}`,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.25rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Diskusi
</h3>
<div className="space-y-3">
{discussions.map((discussion, index) => (
<div
key={index}
className="flex items-start gap-3 p-3 rounded-lg transition-colors"
style={{
backgroundColor: dark ? "#1F2937" : "#F9FAFB",
}}
>
<div className="flex-shrink-0">
<div
className="w-8 h-8 rounded-full flex items-center justify-center"
style={{ backgroundColor: "#DBEAFE" }}
>
<IconMessage
className="w-4 h-4"
style={{ color: "#1E3A5F" }}
stroke={2}
/>
</div>
</div>
<div className="flex-1">
<h4
className="text-sm font-medium"
style={{ color: dark ? "white" : "#1F2937" }}
>
{discussion.title}
</h4>
<p
className="text-xs mt-1"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
>
{discussion.sender} {discussion.date}
</p>
</div>
</div>
))} ))}
</Stack> </div>
) : ( </div>
<Text c="dimmed" ta="center" py="md">
Tidak ada acara hari ini {/* Right Card - Acara Hari Ini */}
</Text> <div
)} className="rounded-xl shadow-sm p-5"
</Card> style={{
</Stack> backgroundColor: dark ? "#141D34" : "white",
border: `1px solid ${dark ? "#141D34" : "white"}`,
borderRadius: "12px",
boxShadow: "0 1px 3px rgba(0,0,0,0.1)",
padding: "1.25rem",
}}
>
<h3
className="text-lg font-semibold mb-4"
style={{ color: dark ? "white" : "#1F2937" }}
>
Acara Hari Ini
</h3>
<div className="flex items-center justify-center h-32">
<p
className="text-sm"
style={{ color: dark ? "#9CA3AF" : "#6B7280" }}
>
Tidak ada acara hari ini
</p>
</div>
</div>
</div>
</div>
); );
}; };
export default KinerjaDivisi; export default KinerjaDivisi;

File diff suppressed because it is too large Load Diff

View File

@@ -1,125 +1,190 @@
import { Card, Title, Text, Space, Button, Group, Alert, Table, ActionIcon, Modal, TextInput, Select, useMantineColorScheme } from '@mantine/core'; import {
import { IconInfoCircle, IconUserPlus, IconTrash, IconEdit, IconUser } from '@tabler/icons-react'; ActionIcon,
import { useState } from 'react'; Alert,
Button,
Card,
Group,
Modal,
Select,
Space,
Table,
Text,
TextInput,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
IconEdit,
IconInfoCircle,
IconTrash,
IconUser,
IconUserPlus,
} from "@tabler/icons-react";
import { useState } from "react";
const AksesDanTimSettings = () => { const AksesDanTimSettings = () => {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
// Sample team members data // Sample team members data
const teamMembers = [ const teamMembers = [
{ id: 1, name: 'Admin Utama', email: 'admin@desa.go.id', role: 'Administrator', status: 'Aktif' }, {
{ id: 2, name: 'Operator Desa', email: 'operator@desa.go.id', role: 'Operator', status: 'Aktif' }, id: 1,
{ id: 3, name: 'Staff Keuangan', email: 'keuangan@desa.go.id', role: 'Keuangan', status: 'Aktif' }, name: "Admin Utama",
{ id: 4, name: 'Staff Umum', email: 'umum@desa.go.id', role: 'Umum', status: 'Nonaktif' }, email: "admin@desa.go.id",
]; role: "Administrator",
status: "Aktif",
},
{
id: 2,
name: "Operator Desa",
email: "operator@desa.go.id",
role: "Operator",
status: "Aktif",
},
{
id: 3,
name: "Staff Keuangan",
email: "keuangan@desa.go.id",
role: "Keuangan",
status: "Aktif",
},
{
id: 4,
name: "Staff Umum",
email: "umum@desa.go.id",
role: "Umum",
status: "Nonaktif",
},
];
const roles = [ const roles = [
{ value: 'administrator', label: 'Administrator' }, { value: "administrator", label: "Administrator" },
{ value: 'operator', label: 'Operator' }, { value: "operator", label: "Operator" },
{ value: 'keuangan', label: 'Keuangan' }, { value: "keuangan", label: "Keuangan" },
{ value: 'umum', label: 'Umum' }, { value: "umum", label: "Umum" },
{ value: 'keamanan', label: 'Keamanan' }, { value: "keamanan", label: "Keamanan" },
]; ];
return ( return (
<Card withBorder radius="md" p="xl" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <Card
<Modal withBorder
opened={opened} radius="md"
onClose={() => setOpened(false)} p="xl"
title="Tambah Anggota Tim" bg={dark ? "#141D34" : "white"}
size="lg" style={{ borderColor: dark ? "#141D34" : "white" }}
> >
<TextInput <Modal
label="Nama Lengkap" opened={opened}
placeholder="Masukkan nama lengkap anggota tim" onClose={() => setOpened(false)}
mb="md" title="Tambah Anggota Tim"
/> size="lg"
<TextInput >
label="Alamat Email" <TextInput
placeholder="Masukkan alamat email" label="Nama Lengkap"
mb="md" placeholder="Masukkan nama lengkap anggota tim"
/> mb="md"
<Select />
label="Peran" <TextInput
placeholder="Pilih peran anggota tim" label="Alamat Email"
data={roles} placeholder="Masukkan alamat email"
mb="md" mb="md"
/> />
<Group justify="flex-end" mt="xl"> <Select
<Button variant="outline" onClick={() => setOpened(false)}>Batal</Button> label="Peran"
<Button>Undang Anggota</Button> placeholder="Pilih peran anggota tim"
</Group> data={roles}
</Modal> mb="md"
/>
<Group justify="flex-end" mt="xl">
<Button variant="outline" onClick={() => setOpened(false)}>
Batal
</Button>
<Button>Undang Anggota</Button>
</Group>
</Modal>
<Title order={2} mb="lg">Akses & Tim</Title> <Title order={2} mb="lg">
<Text color="dimmed" mb="xl">Kelola akses dan anggota tim Anda</Text> Akses & Tim
</Title>
<Text color="dimmed" mb="xl">
Kelola akses dan anggota tim Anda
</Text>
<Space h="lg" /> <Space h="lg" />
<Group justify="space-between" mb="md"> <Group justify="space-between" mb="md">
<Title order={4}>Anggota Tim</Title> <Title order={4}>Anggota Tim</Title>
<Button leftSection={<IconUserPlus size={16} />} onClick={() => setOpened(true)}> <Button
Tambah Anggota leftSection={<IconUserPlus size={16} />}
</Button> onClick={() => setOpened(true)}
</Group> >
Tambah Anggota
</Button>
</Group>
<Table highlightOnHover> <Table highlightOnHover>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>Nama</Table.Th> <Table.Th>Nama</Table.Th>
<Table.Th>Email</Table.Th> <Table.Th>Email</Table.Th>
<Table.Th>Peran</Table.Th> <Table.Th>Peran</Table.Th>
<Table.Th>Status</Table.Th> <Table.Th>Status</Table.Th>
<Table.Th>Aksi</Table.Th> <Table.Th>Aksi</Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{teamMembers.map((member) => ( {teamMembers.map((member) => (
<Table.Tr key={member.id}> <Table.Tr key={member.id}>
<Table.Td> <Table.Td>
<Group gap="sm"> <Group gap="sm">
<IconUser size={20} /> <IconUser size={20} />
<Text>{member.name}</Text> <Text>{member.name}</Text>
</Group> </Group>
</Table.Td> </Table.Td>
<Table.Td>{member.email}</Table.Td> <Table.Td>{member.email}</Table.Td>
<Table.Td> <Table.Td>
<Text fw={500}>{member.role}</Text> <Text fw={500}>{member.role}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text c={member.status === 'Aktif' ? 'green' : 'red'} fw={500}> <Text c={member.status === "Aktif" ? "green" : "red"} fw={500}>
{member.status} {member.status}
</Text> </Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Group> <Group>
<ActionIcon variant="subtle" color="blue"> <ActionIcon variant="subtle" color="blue">
<IconEdit size={16} /> <IconEdit size={16} />
</ActionIcon> </ActionIcon>
<ActionIcon variant="subtle" color="red"> <ActionIcon variant="subtle" color="red">
<IconTrash size={16} /> <IconTrash size={16} />
</ActionIcon> </ActionIcon>
</Group> </Group>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
))} ))}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
<Space h="xl" /> <Space h="xl" />
<Alert icon={<IconInfoCircle size={16} />} title="Informasi" color="blue" mb="md"> <Alert
Administrator memiliki akses penuh ke semua fitur. Peran lainnya memiliki akses terbatas sesuai kebutuhan. icon={<IconInfoCircle size={16} />}
</Alert> title="Informasi"
color="blue"
mb="md"
>
Administrator memiliki akses penuh ke semua fitur. Peran lainnya
memiliki akses terbatas sesuai kebutuhan.
</Alert>
<Group justify="flex-end" mt="xl"> <Group justify="flex-end" mt="xl">
<Button variant="outline">Batal</Button> <Button variant="outline">Batal</Button>
<Button>Simpan Perubahan</Button> <Button>Simpan Perubahan</Button>
</Group> </Group>
</Card> </Card>
); );
}; };
export default AksesDanTimSettings; export default AksesDanTimSettings;

View File

@@ -1,57 +1,90 @@
import { Card, Title, Text, Space, Button, Group, Alert, PasswordInput, Switch, useMantineColorScheme } from '@mantine/core'; import {
import { IconInfoCircle, IconLock } from '@tabler/icons-react'; Alert,
Button,
Card,
Group,
PasswordInput,
Space,
Switch,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconInfoCircle, IconLock } from "@tabler/icons-react";
const KeamananSettings = () => { const KeamananSettings = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
return ( return (
<Card withBorder radius="md" p="xl" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <Card
<Title order={2} mb="lg">Pengaturan Keamanan</Title> withBorder
<Text color="dimmed" mb="xl">Kelola keamanan akun Anda</Text> radius="md"
p="xl"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={2} mb="lg">
Pengaturan Keamanan
</Title>
<Text color="dimmed" mb="xl">
Kelola keamanan akun Anda
</Text>
<Space h="lg" /> <Space h="lg" />
<PasswordInput <PasswordInput
label="Kata Sandi Saat Ini" label="Kata Sandi Saat Ini"
placeholder="Masukkan kata sandi saat ini" placeholder="Masukkan kata sandi saat ini"
mb="md" mb="md"
/> />
<PasswordInput <PasswordInput
label="Kata Sandi Baru" label="Kata Sandi Baru"
placeholder="Masukkan kata sandi baru" placeholder="Masukkan kata sandi baru"
mb="md" mb="md"
/> />
<PasswordInput <PasswordInput
label="Konfirmasi Kata Sandi Baru" label="Konfirmasi Kata Sandi Baru"
placeholder="Konfirmasi kata sandi baru" placeholder="Konfirmasi kata sandi baru"
mb="md" mb="md"
/> />
<Space h="md" /> <Space h="md" />
<Group mb="md"> <Group mb="md">
<Switch label="Verifikasi Dua Langkah" /> <Switch label="Verifikasi Dua Langkah" />
<Switch label="Login Otentikasi Aplikasi" /> <Switch label="Login Otentikasi Aplikasi" />
</Group> </Group>
<Space h="md" /> <Space h="md" />
<Alert icon={<IconLock size={16} />} title="Keamanan" color="orange" mb="md"> <Alert
Gunakan kata sandi yang kuat dan unik. Hindari menggunakan kata sandi yang sama di banyak layanan. icon={<IconLock size={16} />}
</Alert> title="Keamanan"
color="orange"
mb="md"
>
Gunakan kata sandi yang kuat dan unik. Hindari menggunakan kata sandi
yang sama di banyak layanan.
</Alert>
<Alert icon={<IconInfoCircle size={16} />} title="Informasi" color="blue" mb="md"> <Alert
Setelah mengganti kata sandi, Anda akan diminta logout dari semua perangkat. icon={<IconInfoCircle size={16} />}
</Alert> title="Informasi"
color="blue"
mb="md"
>
Setelah mengganti kata sandi, Anda akan diminta logout dari semua
perangkat.
</Alert>
<Group justify="flex-end" mt="xl"> <Group justify="flex-end" mt="xl">
<Button variant="outline">Batal</Button> <Button variant="outline">Batal</Button>
<Button>Perbarui Kata Sandi</Button> <Button>Perbarui Kata Sandi</Button>
</Group> </Group>
</Card> </Card>
); );
}; };
export default KeamananSettings; export default KeamananSettings;

View File

@@ -1,55 +1,86 @@
import { Card, Title, Text, Space, Switch, Group, Alert, Checkbox, Button, useMantineColorScheme } from '@mantine/core'; import {
import { IconInfoCircle } from '@tabler/icons-react'; Alert,
Button,
Card,
Checkbox,
Group,
Space,
Switch,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
const NotifikasiSettings = () => { const NotifikasiSettings = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
return ( return (
<Card withBorder radius="md" p="xl" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <Card
<Title order={2} mb="lg">Pengaturan Notifikasi</Title> withBorder
<Text color="dimmed" mb="xl">Kelola preferensi notifikasi Anda</Text> radius="md"
p="xl"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={2} mb="lg">
Pengaturan Notifikasi
</Title>
<Text color="dimmed" mb="xl">
Kelola preferensi notifikasi Anda
</Text>
<Space h="lg" /> <Space h="lg" />
<Checkbox.Group defaultValue={['email', 'push']} mb="md"> <Checkbox.Group defaultValue={["email", "push"]} mb="md">
<Title order={4} mb="sm">Metode Notifikasi</Title> <Title order={4} mb="sm">
<Group> Metode Notifikasi
<Checkbox value="email" label="Email" /> </Title>
<Checkbox value="push" label="Notifikasi Push" /> <Group>
<Checkbox value="sms" label="SMS" /> <Checkbox value="email" label="Email" />
</Group> <Checkbox value="push" label="Notifikasi Push" />
</Checkbox.Group> <Checkbox value="sms" label="SMS" />
</Group>
</Checkbox.Group>
<Space h="md" /> <Space h="md" />
<Group mb="md"> <Group mb="md">
<Switch label="Notifikasi Email" defaultChecked /> <Switch label="Notifikasi Email" defaultChecked />
<Switch label="Notifikasi Push" defaultChecked /> <Switch label="Notifikasi Push" defaultChecked />
</Group> </Group>
<Space h="md" /> <Space h="md" />
<Title order={4} mb="sm">Jenis Notifikasi</Title> <Title order={4} mb="sm">
<Group align="start"> Jenis Notifikasi
<Switch label="Pengaduan Baru" defaultChecked /> </Title>
<Switch label="Update Status Pengaduan" defaultChecked /> <Group align="start">
<Switch label="Laporan Mingguan" /> <Switch label="Pengaduan Baru" defaultChecked />
<Switch label="Pemberitahuan Keamanan" defaultChecked /> <Switch label="Update Status Pengaduan" defaultChecked />
<Switch label="Aktivitas Akun" defaultChecked /> <Switch label="Laporan Mingguan" />
</Group> <Switch label="Pemberitahuan Keamanan" defaultChecked />
<Switch label="Aktivitas Akun" defaultChecked />
</Group>
<Space h="md" /> <Space h="md" />
<Alert icon={<IconInfoCircle size={16} />} title="Tip" color="blue" mb="md"> <Alert
Anda dapat menyesuaikan frekuensi notifikasi mingguan sesuai kebutuhan Anda. icon={<IconInfoCircle size={16} />}
</Alert> title="Tip"
color="blue"
mb="md"
>
Anda dapat menyesuaikan frekuensi notifikasi mingguan sesuai kebutuhan
Anda.
</Alert>
<Group justify="flex-end" mt="xl"> <Group justify="flex-end" mt="xl">
<Button variant="outline">Batal</Button> <Button variant="outline">Batal</Button>
<Button>Simpan Preferensi</Button> <Button>Simpan Preferensi</Button>
</Group> </Group>
</Card> </Card>
); );
}; };
export default NotifikasiSettings; export default NotifikasiSettings;

View File

@@ -1,58 +1,86 @@
import { Card, Title, Text, Space, TextInput, Select, Button, Group, Switch, Alert, useMantineColorScheme } from '@mantine/core'; import {
import { IconInfoCircle } from '@tabler/icons-react'; Alert,
Button,
Card,
Group,
Select,
Space,
Switch,
Text,
TextInput,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
const UmumSettings = () => { const UmumSettings = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
return ( return (
<Card withBorder radius="md" p="xl" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <Card
<Title order={2} mb="lg">Pengaturan Umum</Title> withBorder
<Text color="dimmed" mb="xl">Kelola pengaturan umum aplikasi Anda</Text> radius="md"
p="xl"
bg={dark ? "#141D34" : "white"}
style={{ borderColor: dark ? "#141D34" : "white" }}
>
<Title order={2} mb="lg">
Pengaturan Umum
</Title>
<Text color="dimmed" mb="xl">
Kelola pengaturan umum aplikasi Anda
</Text>
<Space h="lg" /> <Space h="lg" />
<TextInput <TextInput
label="Nama Aplikasi" label="Nama Aplikasi"
placeholder="Masukkan nama aplikasi" placeholder="Masukkan nama aplikasi"
defaultValue="Dashboard Desa Plus" defaultValue="Dashboard Desa Plus"
mb="md" mb="md"
/> />
<Select <Select
label="Bahasa Aplikasi" label="Bahasa Aplikasi"
data={[ data={[
{ value: 'id', label: 'Indonesia' }, { value: "id", label: "Indonesia" },
{ value: 'en', label: 'English' }, { value: "en", label: "English" },
]} ]}
defaultValue="id" defaultValue="id"
mb="md" mb="md"
/> />
<Select <Select
label="Zona Waktu" label="Zona Waktu"
data={[ data={[
{ value: 'Asia/Jakarta', label: 'Asia/Jakarta (GMT+7)' }, { value: "Asia/Jakarta", label: "Asia/Jakarta (GMT+7)" },
{ value: 'Asia/Makassar', label: 'Asia/Makassar (GMT+8)' }, { value: "Asia/Makassar", label: "Asia/Makassar (GMT+8)" },
{ value: 'Asia/Jayapura', label: 'Asia/Jayapura (GMT+9)' }, { value: "Asia/Jayapura", label: "Asia/Jayapura (GMT+9)" },
]} ]}
defaultValue="Asia/Jakarta" defaultValue="Asia/Jakarta"
mb="md" mb="md"
/> />
<Group mb="md"> <Group mb="md">
<Switch label="Notifikasi Email" defaultChecked /> <Switch label="Notifikasi Email" defaultChecked />
</Group> </Group>
<Alert icon={<IconInfoCircle size={16} />} title="Informasi" color="blue" mb="md"> <Alert
Beberapa pengaturan mungkin memerlukan restart aplikasi untuk diterapkan sepenuhnya. icon={<IconInfoCircle size={16} />}
</Alert> title="Informasi"
color="blue"
mb="md"
>
Beberapa pengaturan mungkin memerlukan restart aplikasi untuk diterapkan
sepenuhnya.
</Alert>
<Group justify="flex-end" mt="xl"> <Group justify="flex-end" mt="xl">
<Button variant="outline">Batal</Button> <Button variant="outline">Batal</Button>
<Button>Simpan Perubahan</Button> <Button>Simpan Perubahan</Button>
</Group> </Group>
</Card> </Card>
); );
}; };
export default UmumSettings; export default UmumSettings;

View File

@@ -1,16 +1,14 @@
import { useNavigate, useLocation } from "@tanstack/react-router";
import { Search, ChevronDown, ChevronUp } from "lucide-react";
import { import {
Stack, Box,
Group, Collapse,
Text, Image,
Badge,
Input, Input,
NavLink as MantineNavLink, NavLink as MantineNavLink,
Box, Stack,
useMantineColorScheme, useMantineColorScheme
Collapse,
} from "@mantine/core"; } from "@mantine/core";
import { useLocation, useNavigate } from "@tanstack/react-router";
import { ChevronDown, ChevronUp, Search } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
interface SidebarProps { interface SidebarProps {
@@ -21,22 +19,28 @@ export function Sidebar({ className }: SidebarProps) {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
const isActiveBg = colorScheme === 'dark' ? "#182949" : "#E6F0FF"; const isActiveBg = colorScheme === "dark" ? "#182949" : "#E6F0FF";
const isActiveBorder = colorScheme === 'dark' ? "#00398D" : "#1F41AE"; const isActiveBorder = colorScheme === "dark" ? "#00398D" : "#1F41AE";
// State for settings submenu collapse // State for settings submenu collapse
const [settingsOpen, setSettingsOpen] = useState( const [settingsOpen, setSettingsOpen] = useState(
location.pathname.startsWith('/dashboard/pengaturan') location.pathname.startsWith("/dashboard/pengaturan"),
); );
// Define menu items with their paths // Define menu items with their paths
const menuItems = [ const menuItems = [
{ name: "Beranda", path: "/dashboard" }, { name: "Beranda", path: "/dashboard" },
{ name: "Kinerja Divisi", path: "/dashboard/kinerja-divisi" }, { name: "Kinerja Divisi", path: "/dashboard/kinerja-divisi" },
{ name: "Pengaduan & Layanan Publik", path: "/dashboard/pengaduan-layanan-publik" }, {
name: "Pengaduan & Layanan Publik",
path: "/dashboard/pengaduan-layanan-publik",
},
{ name: "Jenna Analytic", path: "/dashboard/jenna-analytic" }, { name: "Jenna Analytic", path: "/dashboard/jenna-analytic" },
{ name: "Demografi & Kependudukan", path: "/dashboard/demografi-pekerjaan" }, {
name: "Demografi & Kependudukan",
path: "/dashboard/demografi-pekerjaan",
},
{ name: "Keuangan & Anggaran", path: "/dashboard/keuangan-anggaran" }, { name: "Keuangan & Anggaran", path: "/dashboard/keuangan-anggaran" },
{ name: "Bumdes & UMKM Desa", path: "/dashboard/bumdes" }, { name: "Bumdes & UMKM Desa", path: "/dashboard/bumdes" },
{ name: "Sosial", path: "/dashboard/sosial" }, { name: "Sosial", path: "/dashboard/sosial" },
@@ -53,34 +57,16 @@ export function Sidebar({ className }: SidebarProps) {
]; ];
// Check if any settings submenu is active // Check if any settings submenu is active
const isSettingsActive = settingsItems.some(item => const isSettingsActive = settingsItems.some(
location.pathname === item.path (item) => location.pathname === item.path,
); );
const headerBgColor = colorScheme === "dark" ? "#ebedf0ff" : "#19355E";
return ( return (
<Box className={className}> <Box className={className}>
{/* Logo */} {/* Logo */}
<Box p="md" style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}> <Image src={"/logo-desa-plus.png"} width={201} height={84} />
<Group gap="xs">
<Badge
color="dark"
variant="filled"
size="xl"
radius="md"
py="xs"
px="md"
style={{ fontSize: "1.5rem", fontWeight: "bold" }}
>
DESA
</Badge>
<Badge color="green" variant="filled" size="md" radius="md">
+
</Badge>
</Group>
<Text size="xs" c="dimmed" mt="xs">
Digitalisasi Desa Transparansi Kerja
</Text>
</Box>
{/* Search */} {/* Search */}
<Box p="md"> <Box p="md">
@@ -108,11 +94,13 @@ export function Sidebar({ className }: SidebarProps) {
label={item.name} label={item.name}
active={isActive} active={isActive}
variant="subtle" variant="subtle"
color="blue" color={headerBgColor}
style={{ style={{
background: isActive ? isActiveBg : "transparent", background: isActive ? isActiveBg : "transparent",
fontWeight: isActive ? "bold" : "normal", fontWeight: isActive ? "bold" : "normal",
borderLeft: isActive ? `4px solid ${isActiveBorder}` : "4px solid transparent", borderLeft: isActive
? `4px solid ${isActiveBorder}`
: "4px solid transparent",
borderRadius: "8px", borderRadius: "8px",
transition: "all 200ms ease", transition: "all 200ms ease",
margin: "2px 0", margin: "2px 0",
@@ -121,8 +109,8 @@ export function Sidebar({ className }: SidebarProps) {
body: { body: {
"&:hover": { "&:hover": {
background: "#F1F5F9", background: "#F1F5F9",
} },
} },
}} }}
/> />
); );
@@ -132,7 +120,9 @@ export function Sidebar({ className }: SidebarProps) {
<Box> <Box>
<MantineNavLink <MantineNavLink
onClick={() => setSettingsOpen(!settingsOpen)} onClick={() => setSettingsOpen(!settingsOpen)}
rightSection={settingsOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />} rightSection={
settingsOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />
}
label="Pengaturan" label="Pengaturan"
active={isSettingsActive} active={isSettingsActive}
variant="subtle" variant="subtle"
@@ -140,7 +130,9 @@ export function Sidebar({ className }: SidebarProps) {
style={{ style={{
background: isSettingsActive ? isActiveBg : "transparent", background: isSettingsActive ? isActiveBg : "transparent",
fontWeight: isSettingsActive ? "bold" : "normal", fontWeight: isSettingsActive ? "bold" : "normal",
borderLeft: isSettingsActive ? `4px solid ${isActiveBorder}` : "4px solid transparent", borderLeft: isSettingsActive
? `4px solid ${isActiveBorder}`
: "4px solid transparent",
borderRadius: "8px", borderRadius: "8px",
transition: "all 200ms ease", transition: "all 200ms ease",
margin: "2px 0", margin: "2px 0",
@@ -149,12 +141,16 @@ export function Sidebar({ className }: SidebarProps) {
body: { body: {
"&:hover": { "&:hover": {
background: "#F1F5F9", background: "#F1F5F9",
} },
} },
}} }}
/> />
<Collapse in={settingsOpen}> <Collapse in={settingsOpen}>
<Stack gap={0} ml="lg" style={{ overflowY: 'auto', maxHeight: '200px' }}> <Stack
gap={0}
ml="lg"
style={{ overflowY: "auto", maxHeight: "200px" }}
>
{settingsItems.map((item, index) => { {settingsItems.map((item, index) => {
const isActive = location.pathname === item.path; const isActive = location.pathname === item.path;
return ( return (
@@ -168,7 +164,9 @@ export function Sidebar({ className }: SidebarProps) {
style={{ style={{
background: isActive ? isActiveBg : "transparent", background: isActive ? isActiveBg : "transparent",
fontWeight: isActive ? "bold" : "normal", fontWeight: isActive ? "bold" : "normal",
borderLeft: isActive ? `4px solid ${isActiveBorder}` : "4px solid transparent", borderLeft: isActive
? `4px solid ${isActiveBorder}`
: "4px solid transparent",
borderRadius: "8px", borderRadius: "8px",
transition: "all 200ms ease", transition: "all 200ms ease",
margin: "2px 0", margin: "2px 0",
@@ -177,8 +175,8 @@ export function Sidebar({ className }: SidebarProps) {
body: { body: {
"&:hover": { "&:hover": {
background: "#F1F5F9", background: "#F1F5F9",
} },
} },
}} }}
/> />
); );
@@ -188,5 +186,6 @@ export function Sidebar({ className }: SidebarProps) {
</Box> </Box>
</Stack> </Stack>
</Box> </Box>
); );
} }

View File

@@ -1,289 +1,481 @@
import { useState } from "react"; import {
import { IconAward,
Card, IconBabyCarriage,
Grid, IconCalendarEvent,
GridCol, IconHeartbeat,
Group, IconMedicalCross,
Text, IconSchool,
Title, IconStethoscope,
Progress, IconUsers,
Stack,
useMantineColorScheme,
Badge,
List,
ThemeIcon
} from "@mantine/core";
import {
IconHeartbeat,
IconBabyCarriage,
IconStethoscope,
IconMedicalCross,
IconSchool,
IconBook,
IconCalendarEvent,
IconAward
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useMantineColorScheme } from "@mantine/core";
const SosialPage = () => { const SosialPage = () => {
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === 'dark'; const dark = colorScheme === "dark";
// Sample data for health statistics
const healthStats = {
ibuHamil: 87,
balita: 342,
alertStunting: 12,
posyanduAktif: 8,
};
// Sample data for health progress // Health statistics data
const healthProgress = [ const healthStats = [
{ label: "Imunisasi Lengkap", value: 92, color: "green" }, {
{ label: "Pemeriksaan Rutin", value: 88, color: "blue" }, title: "Ibu Hamil Aktif",
{ label: "Gizi Baik", value: 86, color: "teal" }, value: "87",
{ label: "Target Stunting", value: 14, color: "red" }, subtitle: "Aktif",
]; icon: IconHeartbeat,
},
{
title: "Balita Terdaftar",
value: "342",
subtitle: "Terdaftar",
icon: IconBabyCarriage,
},
{
title: "Alert Stunting",
value: "12",
subtitle: "Perlu perhatian",
icon: IconStethoscope,
alert: true,
},
{
title: "Posyandu Aktif",
value: "8",
subtitle: "Beroperasi",
icon: IconMedicalCross,
},
];
// Sample data for posyandu schedule // Health progress data
const posyanduSchedule = [ const healthProgress = [
{ nama: "Posyandu Mawar", tanggal: "Senin, 15 Feb 2026", jam: "08:00 - 11:00" }, { label: "Imunisasi Lengkap", value: 92 },
{ nama: "Posyandu Melati", tanggal: "Selasa, 16 Feb 2026", jam: "08:00 - 11:00" }, { label: "Pemeriksaan Rutin", value: 88 },
{ nama: "Posyandu Dahlia", tanggal: "Rabu, 17 Feb 2026", jam: "08:00 - 11:00" }, { label: "Gizi Baik", value: 86 },
{ nama: "Posyandu Anggrek", tanggal: "Kamis, 18 Feb 2026", jam: "08:00 - 11:00" }, { label: "Target Stunting", value: 14, isAlert: true },
]; ];
// Sample data for education stats // Posyandu schedule data
const educationStats = { const posyanduSchedule = [
siswa: { {
tk: 125, nama: "Posyandu Barat",
sd: 480, tanggal: "5 Oktober 2025",
smp: 210, jam: "08:00 - 11:00",
sma: 150, },
}, {
sekolah: { nama: "Posyandu Timur",
jumlah: 8, tanggal: "6 Oktober 2025",
guru: 42, jam: "08:00 - 11:00",
} },
}; {
nama: "Posyandu Utara",
tanggal: "7 Oktober 2025",
jam: "08:00 - 11:00",
},
{
nama: "Posyandu Selatan",
tanggal: "8 Oktober 2025",
jam: "08:00 - 11:00",
},
];
// Sample data for scholarships // Education stats data
const scholarshipData = { const educationStats = [
penerima: 45, { level: "TK / PAUD", value: "500" },
dana: "Rp 1.200.000.000", { level: "Siswa SD", value: "458" },
tahunAjaran: "2025/2026", { level: "Siswa SMP", value: "234" },
}; { level: "Siswa SMA", value: "189" },
];
// Sample data for cultural events // School info data
const culturalEvents = [ const schoolInfo = [
{ nama: "Hari Kesaktian Pancasila", tanggal: "1 Oktober 2025", lokasi: "Balai Desa" }, { label: "Lembaga Pendidikan", value: "10" },
{ nama: "Festival Budaya Desa", tanggal: "20 Mei 2026", lokasi: "Lapangan Desa" }, { label: "Tenaga Pengajar", value: "3" },
{ nama: "Perayaan HUT Desa", tanggal: "17 Agustus 2026", lokasi: "Balai Desa" }, ];
];
return ( // Scholarship data
<Stack gap="lg"> const scholarshipData = {
{/* Health Statistics Cards */} penerima: "250+",
<Grid gutter="md"> dana: "1.5M",
<GridCol span={{ base: 12, sm: 6, md: 3 }}> tahunAjaran: "2025/2026",
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> };
<Group justify="space-between" align="center">
<Stack gap={0}>
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
Ibu Hamil Aktif
</Text>
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
{healthStats.ibuHamil}
</Text>
</Stack>
<ThemeIcon variant="light" color="darmasaba-blue" size="xl" radius="xl">
<IconHeartbeat size={24} />
</ThemeIcon>
</Group>
</Card>
</GridCol>
<GridCol span={{ base: 12, sm: 6, md: 3 }}> // Cultural events data
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> const culturalEvents = [
<Group justify="space-between" align="center"> {
<Stack gap={0}> nama: "Lomba Baris Berbaris",
<Text size="sm" c={dark ? "dark.3" : "dimmed"}> tanggal: "1 Desember 2025",
Balita Terdaftar lokasi: "Lapangan Desa",
</Text> },
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}> {
{healthStats.balita} nama: "Lomba Tari Tradisional",
</Text> tanggal: "10 Desember 2025",
</Stack> lokasi: "Banjar Desa",
<ThemeIcon variant="light" color="darmasaba-success" size="xl" radius="xl"> },
<IconBabyCarriage size={24} /> {
</ThemeIcon> nama: "Davoz",
</Group> tanggal: "20 Desember 2025",
</Card> lokasi: "Kantor Desa",
</GridCol> },
];
<GridCol span={{ base: 12, sm: 6, md: 3 }}> return (
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div
<Group justify="space-between" align="center"> className={`min-h-screen py-6 px-4 sm:px-6 lg:px-8 ${
<Stack gap={0}> dark ? "bg-slate-900" : "bg-gray-100"
<Text size="sm" c={dark ? "dark.3" : "dimmed"}> }`}
Alert Stunting >
</Text> <div className="max-w-7xl mx-auto w-full">
<Text size="xl" fw={700} c="red"> {/* Row 1: Top 4 Metrics Cards */}
{healthStats.alertStunting} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
</Text> {healthStats.map((stat, index) => (
</Stack> <div
<ThemeIcon variant="light" color="red" size="xl" radius="xl"> key={index}
<IconStethoscope size={24} /> className={`rounded-xl shadow-sm p-6 ${
</ThemeIcon> dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
</Group> }`}
</Card> >
</GridCol> <div className="flex items-center justify-between">
<div className="flex-1">
<h3
className={`text-sm font-medium mb-1 ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
{stat.title}
</h3>
<p
className={`text-3xl font-bold mb-1 ${
stat.alert
? "text-red-500"
: dark
? "text-white"
: "text-gray-800"
}`}
>
{stat.value}
</p>
<p
className={`text-xs ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
{stat.subtitle}
</p>
</div>
<div className="flex-shrink-0 ml-4">
<div
className={`w-12 h-12 rounded-full flex items-center justify-center text-white ${
stat.alert ? "bg-red-500" : "bg-blue-900"
}`}
>
<stat.icon size={24} />
</div>
</div>
</div>
</div>
))}
</div>
<GridCol span={{ base: 12, sm: 6, md: 3 }}> {/* Row 2: Statistik Kesehatan */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div
<Group justify="space-between" align="center"> className={`rounded-xl shadow-sm p-6 mb-6 ${
<Stack gap={0}> dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
<Text size="sm" c={dark ? "dark.3" : "dimmed"}> }`}
Posyandu Aktif >
</Text> <h3
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}> className={`text-lg font-semibold mb-6 ${
{healthStats.posyanduAktif} dark ? "text-white" : "text-gray-800"
</Text> }`}
</Stack> >
<ThemeIcon variant="light" color="darmasaba-warning" size="xl" radius="xl"> Statistik Kesehatan
<IconMedicalCross size={24} /> </h3>
</ThemeIcon> <div className="space-y-4">
</Group> {healthProgress.map((item, index) => (
</Card> <div key={index}>
</GridCol> <div className="flex items-center justify-between mb-2">
</Grid> <span
className={`text-sm font-medium ${
dark ? "text-white" : "text-gray-800"
}`}
>
{item.label}
</span>
<span
className={`text-sm font-semibold ${
item.isAlert
? "text-red-500"
: dark
? "text-white"
: "text-gray-800"
}`}
>
{item.value}%
</span>
</div>
<div
className={`w-full rounded-full h-2 ${
dark ? "bg-slate-700" : "bg-gray-200"
}`}
>
<div
className={`h-2 rounded-full transition-all ${
item.isAlert ? "bg-red-500" : "bg-blue-900"
}`}
style={{ width: `${item.value}%` }}
/>
</div>
</div>
))}
</div>
</div>
{/* Health Progress Bars */} {/* Row 3: Jadwal Posyandu & Pendidikan */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Statistik Kesehatan</Title> {/* Jadwal Posyandu */}
<Stack gap="md"> <div
{healthProgress.map((item, index) => ( className={`rounded-xl shadow-sm p-6 ${
<div key={index}> dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
<Group justify="space-between" mb={5}> }`}
<Text size="sm" fw={500} c={dark ? "dark.0" : "black"}> >
{item.label} <h3
</Text> className={`text-lg font-semibold mb-4 ${
<Text size="sm" fw={600} c={dark ? "dark.0" : "black"}> dark ? "text-white" : "text-gray-800"
{item.value}% }`}
</Text> >
</Group> Jadwal Posyandu
<Progress </h3>
value={item.value} <div className="space-y-3">
size="lg" {posyanduSchedule.map((item, index) => (
radius="xl" <div
color={item.color} key={index}
/> className={`p-4 rounded-lg ${
</div> dark ? "bg-slate-700" : "bg-gray-50"
))} }`}
</Stack> >
</Card> <div className="flex items-center justify-between">
<div>
<p
className={`text-sm font-medium ${
dark ? "text-white" : "text-gray-800"
}`}
>
{item.nama}
</p>
<p
className={`text-xs mt-1 ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
{item.tanggal}
</p>
</div>
<span
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-900"
>
{item.jam}
</span>
</div>
</div>
))}
</div>
</div>
<Grid gutter="md"> {/* Pendidikan Section */}
{/* Jadwal Posyandu */} <div
<GridCol span={{ base: 12, lg: 6 }}> className={`rounded-xl shadow-sm p-6 ${
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}> dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Jadwal Posyandu</Title> }`}
<Stack gap="sm"> >
{posyanduSchedule.map((item, index) => ( <h3
<Card key={index} p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }} h="100%"> className={`text-lg font-semibold mb-4 ${
<Group justify="space-between"> dark ? "text-white" : "text-gray-800"
<Stack gap={0}> }`}
<Text fw={500} c={dark ? "dark.0" : "black"}>{item.nama}</Text> >
<Text size="sm" c={dark ? "dark.0" : "black"}>{item.tanggal}</Text> Pendidikan
</Stack> </h3>
<Badge variant="light" color="darmasaba-blue"> <div className="space-y-3 mb-6">
{item.jam} {educationStats.map((item, index) => (
</Badge> <div
</Group> key={index}
</Card> className={`flex items-center justify-between py-2 ${
))} dark ? "border-b border-slate-700" : "border-b border-gray-100"
</Stack> }`}
</Card> >
</GridCol> <span className="text-sm" style={{ color: dark ? "#9CA3AF" : "#6B7280" }}>
{item.level}
</span>
<span
className={`text-sm font-semibold ${
dark ? "text-white" : "text-gray-800"
}`}
>
{item.value}
</span>
</div>
))}
</div>
{/* Pendidikan */} {/* Info Sekolah */}
<GridCol span={{ base: 12, lg: 6 }}> <h4
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> className={`text-base font-semibold mb-4 ${
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Pendidikan</Title> dark ? "text-white" : "text-gray-800"
<Stack gap="md"> }`}
<Group justify="space-between"> >
<Text fw={500} c={dark ? "dark.0" : "black"}>TK / PAUD</Text> Info Sekolah
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.tk}</Text> </h4>
</Group> <div className="space-y-3">
<Group justify="space-between"> {schoolInfo.map((item, index) => (
<Text fw={500} c={dark ? "dark.0" : "black"}>SD</Text> <div
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.sd}</Text> key={index}
</Group> className={`flex items-center justify-between py-3 px-4 rounded-lg ${
<Group justify="space-between"> dark ? "bg-slate-700" : "bg-gray-50"
<Text fw={500} c={dark ? "dark.0" : "black"}>SMP</Text> }`}
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.smp}</Text> >
</Group> <span className="text-sm" style={{ color: dark ? "#9CA3AF" : "#6B7280" }}>
<Group justify="space-between"> {item.label}
<Text fw={500} c={dark ? "dark.0" : "black"}>SMA</Text> </span>
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.sma}</Text> <span
</Group> className={`text-lg font-bold ${
dark ? "text-white" : "text-gray-800"
<Card withBorder radius="md" p="md" mt="md" bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}> }`}
<Group justify="space-between"> >
<Text fw={500} c={dark ? "dark.0" : "black"}>Jumlah Lembaga Pendidikan</Text> {item.value}
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.sekolah.jumlah}</Text> </span>
</Group> </div>
<Group justify="space-between" mt="sm"> ))}
<Text fw={500} c={dark ? "dark.0" : "black"}>Jumlah Tenaga Pengajar</Text> </div>
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.sekolah.guru}</Text> </div>
</Group> </div>
</Card>
</Stack>
</Card>
</GridCol>
</Grid>
<Grid gutter="md"> {/* Row 4: Beasiswa Desa & Kalender Event Budaya */}
{/* Beasiswa Desa */} <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<GridCol span={{ base: 12, lg: 6 }}> {/* Beasiswa Desa */}
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> <div
<Group justify="space-between" align="center"> className={`rounded-xl shadow-sm p-6 ${
<Stack gap={0}> dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Beasiswa Desa</Text> }`}
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>Penerima: {scholarshipData.penerima}</Text> >
</Stack> <div className="flex items-center justify-between mb-6">
<ThemeIcon variant="light" color="darmasaba-success" size="xl" radius="xl"> <h3
<IconAward size={24} /> className={`text-lg font-semibold ${
</ThemeIcon> dark ? "text-white" : "text-gray-800"
</Group> }`}
<Text mt="md" c={dark ? "dark.0" : "black"}>Dana Tersalurkan: <Text span fw={700}>{scholarshipData.dana}</Text></Text> >
<Text mt="sm" c={dark ? "dark.3" : "dimmed"}>Tahun Ajaran: {scholarshipData.tahunAjaran}</Text> Beasiswa Desa
</Card> </h3>
</GridCol> <div
className="w-12 h-12 rounded-full flex items-center justify-center text-white bg-green-500"
>
<IconAward size={24} />
</div>
</div>
{/* Kalender Event Budaya */} {/* Two centered metrics */}
<GridCol span={{ base: 12, lg: 6 }}> <div className="grid grid-cols-2 gap-4 mb-6">
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%"> <div
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Kalender Event Budaya</Title> className={`p-4 rounded-lg text-center ${
<List spacing="sm"> dark ? "bg-slate-700" : "bg-gray-50"
{culturalEvents.map((event, index) => ( }`}
<List.Item key={index} icon={ >
<ThemeIcon color="darmasaba-blue" size={24} radius="xl"> <p
<IconCalendarEvent size={12} /> className={`text-3xl font-bold mb-1 ${
</ThemeIcon> dark ? "text-white" : "text-gray-800"
}> }`}
<Group justify="space-between"> >
<Text fw={500} c={dark ? "dark.0" : "black"}>{event.nama}</Text> {scholarshipData.penerima}
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{event.lokasi}</Text> </p>
</Group> <p
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{event.tanggal}</Text> className={`text-xs ${
</List.Item> dark ? "text-gray-400" : "text-gray-500"
))} }`}
</List> >
</Card> Penerima Beasiswa
</GridCol> </p>
</Grid> </div>
</Stack> <div
); className={`p-4 rounded-lg text-center ${
dark ? "bg-slate-700" : "bg-gray-50"
}`}
>
<p
className={`text-3xl font-bold mb-1 text-green-500`}
>
{scholarshipData.dana}
</p>
<p
className={`text-xs ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
Dana Tersalurkan
</p>
</div>
</div>
{/* Footer text */}
<p
className={`text-center text-sm ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
Tahun Ajaran {scholarshipData.tahunAjaran}
</p>
</div>
{/* Kalender Event Budaya */}
<div
className={`rounded-xl shadow-sm p-6 ${
dark ? "bg-slate-800 border border-slate-800" : "bg-white border border-white"
}`}
>
<h3
className={`text-lg font-semibold mb-4 ${
dark ? "text-white" : "text-gray-800"
}`}
>
Kalender Event Budaya
</h3>
<div className="space-y-4">
{culturalEvents.map((event, index) => (
<div
key={index}
className={`flex items-start gap-3 p-4 rounded-lg ${
dark ? "bg-slate-700" : "bg-gray-50"
}`}
>
<div
className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 bg-blue-100 text-blue-900"
>
<IconCalendarEvent size={20} />
</div>
<div className="flex-1">
<p
className={`text-sm font-medium ${
dark ? "text-white" : "text-gray-800"
}`}
>
{event.nama}
</p>
<p
className={`text-xs mt-1 ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
{event.tanggal}
</p>
<p
className={`text-xs mt-1 ${
dark ? "text-gray-400" : "text-gray-500"
}`}
>
Location: {event.lokasi}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}; };
export default SosialPage; export default SosialPage;

View File

@@ -2,7 +2,6 @@ import {
Box, Box,
Card as MantineCard, Card as MantineCard,
type CardProps as MantineCardProps, type CardProps as MantineCardProps,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import type React from "react"; import type React from "react";

View File

@@ -1,90 +1,86 @@
import { Card, useMantineTheme, useComputedColorScheme } from '@mantine/core'; import type { CardProps } from "@mantine/core";
import type { CardProps } from '@mantine/core'; import { Card, useComputedColorScheme, useMantineTheme } from "@mantine/core";
import type { ReactNode } from 'react'; import type { ReactNode } from "react";
interface HelpCardProps extends CardProps { interface HelpCardProps extends CardProps {
children: ReactNode; children: ReactNode;
icon?: ReactNode; icon?: ReactNode;
title?: string; title?: string;
minHeight?: string | number; // Allow specifying a minimum height minHeight?: string | number; // Allow specifying a minimum height
} }
export const HelpCard = ({ export const HelpCard = ({
children, children,
icon, icon,
title, title,
minHeight = 'auto', // Default to auto, but allow override minHeight = "auto", // Default to auto, but allow override
...props ...props
}: HelpCardProps) => { }: HelpCardProps) => {
const theme = useMantineTheme(); const theme = useMantineTheme();
const colorScheme = useComputedColorScheme('light'); const colorScheme = useComputedColorScheme("light");
const isDark = colorScheme === 'dark'; const isDark = colorScheme === "dark";
return ( return (
<Card <Card
shadow="sm" shadow="sm"
padding="xl" padding="xl"
radius="md" radius="md"
withBorder withBorder
style={{ style={{
backgroundColor: isDark ? theme.colors.dark[7] : theme.white, backgroundColor: isDark ? theme.colors.dark[7] : theme.white,
borderRadius: '16px', borderRadius: "16px",
transition: 'transform 0.2s ease, box-shadow 0.2s ease', transition: "transform 0.2s ease, box-shadow 0.2s ease",
border: `1px solid ${ border: `1px solid ${
isDark ? theme.colors.dark[4] : theme.colors.gray[3] isDark ? theme.colors.dark[4] : theme.colors.gray[3]
}`, }`,
minHeight, // Apply the minimum height minHeight, // Apply the minimum height
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
}} }}
{...props} {...props}
> >
{(icon || title) && ( {(icon || title) && (
<div <div
style={{ style={{
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
gap: '12px', gap: "12px",
marginBottom: '16px', marginBottom: "16px",
}} }}
> >
{icon && ( {icon && (
<div <div
style={{ style={{
backgroundColor: isDark backgroundColor: isDark
? theme.colors.blue[8] ? theme.colors.blue[8]
: theme.colors.blue[0], : theme.colors.blue[0],
borderRadius: '8px', borderRadius: "8px",
padding: '8px', padding: "8px",
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
justifyContent: 'center', justifyContent: "center",
}} }}
> >
{icon} {icon}
</div> </div>
)} )}
{title && ( {title && (
<h3 <h3
style={{ style={{
margin: 0, margin: 0,
fontSize: '16px', fontSize: "16px",
fontWeight: 600, fontWeight: 600,
color: isDark color: isDark ? theme.colors.dark[0] : theme.colors.dark[9],
? theme.colors.dark[0] }}
: theme.colors.dark[9], >
}} {title}
> </h3>
{title} )}
</h3> </div>
)} )}
</div>
)}
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>{children}</div>
{children} </Card>
</div> );
</Card>
);
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -14,10 +14,9 @@ import { Inspector } from "react-dev-inspector";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { routeTree } from "./routeTree.gen"; import { routeTree } from "./routeTree.gen";
import "./index.css"; import "./index.css";
import '@mantine/charts/styles.css'; import "@mantine/charts/styles.css";
import { IS_DEV, VITE_PUBLIC_URL } from "./utils/env"; import { IS_DEV, VITE_PUBLIC_URL } from "./utils/env";
// Create a new router instance // Create a new router instance
export const router = createRouter({ export const router = createRouter({
routeTree, routeTree,
@@ -102,8 +101,6 @@ const theme = createTheme({
primaryColor: "darmasaba-blue", primaryColor: "darmasaba-blue",
}); });
const InspectorWrapper = IS_DEV const InspectorWrapper = IS_DEV
? Inspector ? Inspector
: ({ children }: { children: React.ReactNode }) => <>{children}</>; : ({ children }: { children: React.ReactNode }) => <>{children}</>;

View File

@@ -1 +1,100 @@
@import "tailwindcss"; @import "tailwindcss";
/* Custom CSS variables for Tailwind */
:root {
/* Darmasaba Navy Colors */
--darmasaba-navy-50: #E1E4F2;
--darmasaba-navy-100: #B9C2DD;
--darmasaba-navy-200: #91A0C9;
--darmasaba-navy-300: #697EBA;
--darmasaba-navy-400: #4C6CAE;
--darmasaba-navy-500: #3B5B97;
--darmasaba-navy-600: #2C497F;
--darmasaba-navy-700: #1E3766;
--darmasaba-navy-800: #12264D;
--darmasaba-navy-900: #071833;
--darmasaba-navy: #1E3A5F;
/* Darmasaba Blue Colors */
--darmasaba-blue-50: #E3F0FF;
--darmasaba-blue-100: #B6D9FF;
--darmasaba-blue-200: #89C2FF;
--darmasaba-blue-300: #5CA9FF;
--darmasaba-blue-400: #3B8FFF;
--darmasaba-blue-500: #237AE0;
--darmasaba-blue-600: #1C6BBF;
--darmasaba-blue-700: #155BA0;
--darmasaba-blue-800: #0E4980;
--darmasaba-blue-900: #073260;
--darmasaba-blue: #3B82F6;
/* Darmasaba Success Colors */
--darmasaba-success-50: #E3F9E7;
--darmasaba-success-100: #BFEEC7;
--darmasaba-success-200: #9BD8A7;
--darmasaba-success-300: #77C387;
--darmasaba-success-400: #5DB572;
--darmasaba-success-500: #499A5D;
--darmasaba-success-600: #3C7F4A;
--darmasaba-success-700: #2F6438;
--darmasaba-success-800: #234926;
--darmasaba-success-900: #17301B;
--darmasaba-success: #22C55E;
/* Darmasaba Warning Colors */
--darmasaba-warning-50: #FFF8E1;
--darmasaba-warning-100: #FEE7B3;
--darmasaba-warning-200: #FDD785;
--darmasaba-warning-300: #FDC757;
--darmasaba-warning-400: #FBBF3B;
--darmasaba-warning-500: #E1AC23;
--darmasaba-warning-600: #C2981D;
--darmasaba-warning-700: #A38418;
--darmasaba-warning-800: #856F12;
--darmasaba-warning-900: #675A0D;
--darmasaba-warning: #FACC15;
/* Darmasaba Danger Colors */
--darmasaba-danger-50: #FFE3E3;
--darmasaba-danger-100: #FFBABA;
--darmasaba-danger-200: #FF9191;
--darmasaba-danger-300: #FF6868;
--darmasaba-danger-400: #FA4B4B;
--darmasaba-danger-500: #E03333;
--darmasaba-danger-600: #C22A2A;
--darmasaba-danger-700: #A32020;
--darmasaba-danger-800: #851616;
--darmasaba-danger-900: #670C0C;
--darmasaba-danger: #EF4444;
/* Darmasaba Background */
--darmasaba-background: #F5F8FB;
/* Standard colors for dark mode */
--slate-900: #0F172A;
--slate-800: #1E293B;
--slate-700: #334155;
--slate-600: #475569;
--gray-50: #F9FAFB;
--gray-100: #F3F4F6;
--gray-200: #E5E7EB;
--gray-600: #4B5563;
--gray-700: #1F2937;
--gray-400: #9CA3AF;
--gray-500: #6B7280;
--gray-800: #1F2937;
--blue-50: #EFF6FF;
--blue-100: #DBEAFE;
--blue-900: #1E3A5F;
--red-500: #EF4444;
--green-500: #22C55E;
}
/* Dark mode support */
[data-mantine-color-scheme="dark"] {
color-scheme: dark;
}
[data-mantine-color-scheme="light"] {
color-scheme: light;
}

View File

@@ -6,8 +6,22 @@ import { Elysia } from "elysia";
import api from "./api"; import api from "./api";
import { openInEditor } from "./utils/open-in-editor"; import { openInEditor } from "./utils/open-in-editor";
const PORT = process.env.PORT || 3000;
const isProduction = process.env.NODE_ENV === "production"; const isProduction = process.env.NODE_ENV === "production";
// Auto-seed database in production (ensure admin user exists)
if (isProduction && process.env.ADMIN_EMAIL) {
try {
console.log("🌱 Running database seed in production...");
const { runSeed } = await import("../prisma/seed.ts");
await runSeed();
} catch (error) {
console.error("⚠️ Production seed failed:", error);
// Don't crash the server if seed fails
}
}
const app = new Elysia().use(api); const app = new Elysia().use(api);
if (!isProduction) { if (!isProduction) {
@@ -81,10 +95,27 @@ if (!isProduction) {
getHeader(name: string) { getHeader(name: string) {
return this.headers[name.toLowerCase()]; return this.headers[name.toLowerCase()];
}, },
writeHead(code: number, headers: Record<string, string>) {
this.statusCode = code;
Object.assign(this.headers, headers);
},
write(chunk: any, callback?: () => void) {
// Collect chunks for streaming responses
if (!this._chunks) this._chunks = [];
this._chunks.push(chunk);
if (callback) callback();
return true; // Indicate we can accept more data
},
headers: {} as Record<string, string>, headers: {} as Record<string, string>,
end(data: any) { end(data: any) {
// Handle potential Buffer or string data from Vite // Handle potential Buffer or string data from Vite
let body = data; let body = data;
// If we have collected chunks from write() calls, combine them
if (this._chunks && this._chunks.length > 0) {
body = Buffer.concat(this._chunks);
}
if (data instanceof Uint8Array) { if (data instanceof Uint8Array) {
body = data; body = data;
} else if (typeof data === "string") { } else if (typeof data === "string") {
@@ -144,6 +175,11 @@ if (!isProduction) {
if (fs.existsSync(srcPath)) { if (fs.existsSync(srcPath)) {
filePath = srcPath; filePath = srcPath;
} }
// Check public folder for static assets
const publicPath = path.join("public", pathname);
if (fs.existsSync(publicPath)) {
filePath = publicPath;
}
} }
// 2. If not found and looks like an asset (has extension), try root of dist or src // 2. If not found and looks like an asset (has extension), try root of dist or src
@@ -159,8 +195,18 @@ if (!isProduction) {
) { ) {
filePath = fallbackDistPath; filePath = fallbackDistPath;
} }
// Try public folder
else {
const fallbackPublicPath = path.join("public", filename);
if (
fs.existsSync(fallbackPublicPath) &&
fs.statSync(fallbackPublicPath).isFile()
) {
filePath = fallbackPublicPath;
}
}
// Special handling for PWA files in src // Special handling for PWA files in src
else if (pathname.includes("assetlinks.json")) { if (pathname.includes("assetlinks.json")) {
const srcFilename = pathname.includes("assetlinks.json") const srcFilename = pathname.includes("assetlinks.json")
? ".well-known/assetlinks.json" ? ".well-known/assetlinks.json"
: filename; : filename;
@@ -198,10 +244,11 @@ if (!isProduction) {
}); });
} }
app.listen(3000); app.listen(PORT);
console.log( console.log(
`🚀 Server running at http://localhost:3000 in ${isProduction ? "production" : "development"} mode`, `🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`,
); );
export type ApiApp = typeof app; export type ApiApp = typeof app;

View File

@@ -66,10 +66,10 @@ const routeRules: RouteRule[] = [
redirectTo: "/signin", redirectTo: "/signin",
}, },
{ {
match: (p) => p === "/dashboard" || p.startsWith("/dashboard/"), match: (p) => p === "/admin" || p.startsWith("/admin/"),
requireAuth: true, requireAuth: true,
requiredRole: "admin", requiredRole: "admin",
redirectTo: "/profile", redirectTo: "/signin",
}, },
]; ];

View File

@@ -1,5 +1,4 @@
/** biome-ignore-all lint/suspicious/noExplicitAny: <explanation */ /** biome-ignore-all lint/suspicious/noExplicitAny: <explanation */
import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
import { authStore } from "@/store/auth"; import { authStore } from "@/store/auth";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import "@mantine/dates/styles.css"; import "@mantine/dates/styles.css";
@@ -7,10 +6,22 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({ export const Route = createRootRoute({
component: RootComponent, component: RootComponent,
beforeLoad: protectedRouteMiddleware, beforeLoad: async () => {
onEnter({ context }) { // Fetch session but don't block navigation
authStore.user = context?.user as any; try {
authStore.session = context?.session as any; const res = await fetch("/api/session", {
method: "GET",
credentials: "include",
});
if (res.ok) {
const { data } = await res.json();
authStore.user = data?.user;
authStore.session = data;
}
} catch {
// Ignore errors, allow public access
}
return {};
}, },
}); });

View File

@@ -154,7 +154,6 @@ function DashboardLayout() {
</Group> </Group>
<Group gap="md"> <Group gap="md">
<Menu <Menu
shadow="md" shadow="md"
width={200} width={200}

View File

@@ -1,7 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import HelpPage from '@/components/help-page' import HelpPage from "@/components/help-page";
export const Route = createFileRoute('/dashboard/bantuan')({
component: HelpPage,
})
export const Route = createFileRoute("/dashboard/bantuan")({
component: HelpPage,
});

View File

@@ -1,7 +1,6 @@
import BumdesPage from '@/components/bumdes-page' import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute } from '@tanstack/react-router' import BumdesPage from "@/components/bumdes-page";
export const Route = createFileRoute('/dashboard/bumdes')({
component: BumdesPage,
})
export const Route = createFileRoute("/dashboard/bumdes")({
component: BumdesPage,
});

View File

@@ -1,7 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import DemografiPekerjaan from '../../components/demografi-pekerjaan' import DemografiPekerjaan from "../../components/demografi-pekerjaan";
export const Route = createFileRoute('/dashboard/demografi-pekerjaan')({
component: DemografiPekerjaan,
})
export const Route = createFileRoute("/dashboard/demografi-pekerjaan")({
component: DemografiPekerjaan,
});

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import JennaAnalytic from '@/components/jenna-analytic' import JennaAnalytic from "@/components/jenna-analytic";
export const Route = createFileRoute('/dashboard/jenna-analytic')({ export const Route = createFileRoute("/dashboard/jenna-analytic")({
component: JennaAnalytic, component: JennaAnalytic,
}) });

View File

@@ -1,7 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import KeamananPage from '@/components/keamanan-page' import KeamananPage from "@/components/keamanan-page";
export const Route = createFileRoute('/dashboard/keamanan')({
component: KeamananPage,
})
export const Route = createFileRoute("/dashboard/keamanan")({
component: KeamananPage,
});

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import KeuanganAnggaran from '@/components/keuangan-anggaran' import KeuanganAnggaran from "@/components/keuangan-anggaran";
export const Route = createFileRoute('/dashboard/keuangan-anggaran')({ export const Route = createFileRoute("/dashboard/keuangan-anggaran")({
component: KeuanganAnggaran, component: KeuanganAnggaran,
}) });

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import AksesDanTimSettings from '@/components/pengaturan/akses-dan-tim' import AksesDanTimSettings from "@/components/pengaturan/akses-dan-tim";
export const Route = createFileRoute('/dashboard/pengaturan/akses-dan-tim')({ export const Route = createFileRoute("/dashboard/pengaturan/akses-dan-tim")({
component: AksesDanTimSettings, component: AksesDanTimSettings,
}) });

View File

@@ -1,7 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import KeamananSettings from '@/components/pengaturan/keamanan' import KeamananSettings from "@/components/pengaturan/keamanan";
export const Route = createFileRoute('/dashboard/pengaturan/keamanan')({
component: KeamananSettings,
})
export const Route = createFileRoute("/dashboard/pengaturan/keamanan")({
component: KeamananSettings,
});

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import NotifikasiSettings from '@/components/pengaturan/notifikasi' import NotifikasiSettings from "@/components/pengaturan/notifikasi";
export const Route = createFileRoute('/dashboard/pengaturan/notifikasi')({ export const Route = createFileRoute("/dashboard/pengaturan/notifikasi")({
component: NotifikasiSettings, component: NotifikasiSettings,
}) });

View File

@@ -1,9 +1,9 @@
import { createFileRoute, Outlet } from '@tanstack/react-router'; import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute('/dashboard/pengaturan')({ export const Route = createFileRoute("/dashboard/pengaturan")({
component: () => ( component: () => (
<div className="p-2"> <div className="p-2">
<Outlet /> <Outlet />
</div> </div>
), ),
}); });

View File

@@ -1,7 +1,6 @@
import UmumSettings from '@/components/pengaturan/umum' import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute } from '@tanstack/react-router' import UmumSettings from "@/components/pengaturan/umum";
export const Route = createFileRoute('/dashboard/pengaturan/umum')({
component: UmumSettings,
})
export const Route = createFileRoute("/dashboard/pengaturan/umum")({
component: UmumSettings,
});

View File

@@ -1,19 +1,39 @@
import { createFileRoute, Outlet } from "@tanstack/react-router"; import {
AppShell,
Burger,
Group,
useMantineColorScheme,
useMantineTheme,
} from "@mantine/core";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router";
import { useEffect } from "react";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar"; import { Sidebar } from "@/components/sidebar";
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
export const Route = createFileRoute("/dashboard")({ export const Route = createFileRoute("/dashboard")({
component: RouteComponent, component: RouteComponent,
}); });
function RouteComponent() { function RouteComponent() {
const [opened, { toggle }] = useDisclosure(); const [opened, { toggle, close }] = useDisclosure();
const { colorScheme } = useMantineColorScheme(); const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === 'dark' ? "#11192D" : "#19355E"; const theme = useMantineTheme();
const navbarBgColor = colorScheme === 'dark' ? "#11192D" : "white"; const routerState = useRouterState();
const mainBgColor = colorScheme === 'dark' ? "#11192D" : "#edf3f8ff";
const isMobile = useMediaQuery("(max-width: 48em)");
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
// ✅ AUTO CLOSE NAVBAR ON ROUTE CHANGE (MOBILE ONLY)
useEffect(() => {
if (isMobile && opened) {
close();
}
}, [routerState.location.pathname]);
return ( return (
<AppShell <AppShell
header={{ height: 60 }} header={{ height: 60 }}
@@ -25,14 +45,29 @@ function RouteComponent() {
padding="md" padding="md"
> >
<AppShell.Header bg={headerBgColor}> <AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md"> <Group
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" /> h="100%"
px="lg"
align="center"
wrap="nowrap"
>
<Burger
opened={opened}
onClick={toggle}
hiddenFrom="sm"
size="sm"
/>
<Header /> <Header />
</Group> </Group>
</AppShell.Header> </AppShell.Header>
<AppShell.Navbar p="md" bg={navbarBgColor} style={{ display: 'flex', flexDirection: 'column' }}> <AppShell.Navbar
<div style={{ flex: 1, overflowY: 'auto' }}> p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar /> <Sidebar />
</div> </div>
</AppShell.Navbar> </AppShell.Navbar>

View File

@@ -1,8 +1,6 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import SocialPage from '@/components/sosial-page' import SocialPage from "@/components/sosial-page";
export const Route = createFileRoute('/dashboard/sosial')({
component: SocialPage,
})
export const Route = createFileRoute("/dashboard/sosial")({
component: SocialPage,
});

View File

@@ -1,788 +1,7 @@
import { import { createFileRoute, redirect } from "@tanstack/react-router";
ActionIcon,
Avatar,
Box,
Button,
Card,
Container,
Grid,
Group,
Image,
Paper,
rem,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
Transition,
useMantineColorScheme,
} from "@mantine/core";
import {
IconApi,
IconBolt,
IconBrandGithub,
IconBrandLinkedin,
IconBrandTwitter,
IconChevronRight,
IconLock,
IconMoon,
IconRocket,
IconShield,
IconStack2,
IconSun,
} from "@tabler/icons-react";
import { createFileRoute, Link } from "@tanstack/react-router";
import { useEffect, useState } from "react";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: HomePage, beforeLoad: () => {
throw redirect({ to: "/dashboard" });
},
}); });
// Navigation items
const NAV_ITEMS = [
{ label: "Home", link: "/" },
{ label: "Features", link: "#features" },
{ label: "Testimonials", link: "#testimonials" },
{ label: "Pricing", link: "/pricing" },
{ label: "Contact", link: "/contact" },
];
// Features data
const FEATURES = [
{
icon: IconBolt,
title: "Lightning Fast",
description: "Built on Bun runtime for exceptional performance and speed.",
},
{
icon: IconShield,
title: "Secure by Design",
description:
"Enterprise-grade authentication with Better Auth integration.",
},
{
icon: IconApi,
title: "RESTful API",
description:
"Full-featured API with Elysia.js for seamless backend operations.",
},
{
icon: IconStack2,
title: "Modern Stack",
description: "React 19, TanStack Router, and Mantine UI for the best DX.",
},
{
icon: IconLock,
title: "API Key Auth",
description: "Secure API key management for external integrations.",
},
{
icon: IconRocket,
title: "Production Ready",
description: "Type-safe, tested, and optimized for production deployment.",
},
];
// Testimonials data
const TESTIMONIALS = [
{
id: "testimonial-1",
name: "Alex Johnson",
role: "Lead Developer",
content:
"This template saved us weeks of setup time. The architecture is clean and well-thought-out.",
avatar:
"https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
{
id: "testimonial-2",
name: "Sarah Williams",
role: "CTO",
content:
"The performance improvements we saw after switching to this stack were remarkable. Highly recommended!",
avatar:
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
{
id: "testimonial-3",
name: "Michael Chen",
role: "Product Manager",
content:
"The developer experience is top-notch. Everything is well-documented and easy to extend.",
avatar:
"https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=200&q=80",
},
];
function NavigationBar() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<Box
h={70}
px="md"
style={{
borderBottom: "1px solid var(--mantine-color-gray-2)",
transition: "all 0.3s ease",
boxShadow: scrolled ? "0 2px 10px rgba(0,0,0,0.1)" : "none",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Group h="100%" justify="space-between">
<Group>
<Link to="/" style={{ textDecoration: "none" }}>
<Title order={3} c="blue">
BunStack
</Title>
</Link>
<Group ml={50} visibleFrom="sm" gap="lg">
{NAV_ITEMS.map((item) => {
const isActive = window.location.pathname === item.link;
return (
<Box
key={item.label}
component={Link}
to={item.link}
style={{
textDecoration: "none",
fontSize: rem(16),
padding: `${rem(8)} ${rem(12)}`,
borderRadius: rem(6),
transition: "all 0.2s ease",
color: isActive
? "var(--mantine-color-blue-6)"
: "var(--mantine-color-dimmed)",
fontWeight: 500,
cursor: "pointer",
display: "block",
}}
className="nav-item"
>
{item.label}
</Box>
);
})}
</Group>
</Group>
<Group>
<ActionIcon
variant="default"
onClick={() => toggleColorScheme()}
size="lg"
>
{colorScheme === "dark" ? (
<IconSun size={18} />
) : (
<IconMoon size={18} />
)}
</ActionIcon>
<Button component={Link} to="/signin" variant="light" size="sm">
Sign In
</Button>
<Button component={Link} to="/signup" size="sm">
Get Started
</Button>
</Group>
</Group>
</Box>
);
}
function HeroSection() {
const [loaded, setLoaded] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
useEffect(() => {
setLoaded(true);
}, []);
// Simulate delay for image transition
useEffect(() => {
const timer = setTimeout(() => {
setImageLoaded(true);
}, 200);
return () => clearTimeout(timer);
}, []);
return (
<Box
pt={rem(140)} // Adjusted padding for simpler header
pb={rem(60)}
>
<Container size="lg">
<Grid gutter={{ base: rem(40), md: rem(80) }} align="center">
<Grid.Col span={{ base: 12, md: 6 }}>
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Stack gap="xl" style={styles}>
<Title
order={1}
style={{
fontSize: rem(48),
fontWeight: 900,
lineHeight: 1.2,
}}
>
Build Faster with{" "}
<Text span c="blue" inherit>
Bun Stack
</Text>
</Title>
<Text size="xl" c="dimmed">
A modern, full-stack React template powered by Bun,
Elysia.js, and TanStack Router. Ship your ideas faster than
ever.
</Text>
<Group gap="md">
<Button
component={Link}
to="/admin"
size="lg"
variant="filled"
rightSection={<IconRocket size="1.25rem" />}
>
Get Started
</Button>
<Button
component={Link}
to="/docs"
size="lg"
variant="outline"
>
Learn More
</Button>
</Group>
</Stack>
)}
</Transition>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 6 }}>
<Transition
mounted={imageLoaded}
transition="slide-left"
duration={800}
timingFunction="ease"
>
{(styles) => (
<Paper shadow="xl" radius="lg" p="md" withBorder style={styles}>
<Image
src="https://images.unsplash.com/photo-1555066931-4365d14bab8c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1170&q=80"
alt="Code editor showing Bun Stack code"
radius="md"
/>
</Paper>
)}
</Transition>
</Grid.Col>
</Grid>
</Container>
</Box>
);
}
function AnimatedFeatureCard({
feature,
index,
isVisible,
}: {
feature: (typeof FEATURES)[number];
index: number;
isVisible: boolean;
}) {
const [isDelayedVisible, setIsDelayedVisible] = useState(isVisible);
useEffect(() => {
if (isVisible) {
const timer = setTimeout(() => {
setIsDelayedVisible(true);
}, index * 100);
return () => clearTimeout(timer);
}
}, [isVisible, index]);
return (
<Transition
mounted={isDelayedVisible}
transition="slide-up"
duration={500}
timingFunction="ease"
>
{(styles) => (
<Card
className="feature-card"
padding="lg"
radius="md"
withBorder
shadow="sm"
style={styles}
>
<ThemeIcon variant="light" color="blue" size={60} radius="md">
<feature.icon size="1.75rem" />
</ThemeIcon>
<Stack gap={8} mt="md">
<Title order={4}>{feature.title}</Title>
<Text size="sm" c="dimmed" lh={1.5}>
{feature.description}
</Text>
</Stack>
</Card>
)}
</Transition>
);
}
function FeaturesSection() {
const [visibleFeatures, setVisibleFeatures] = useState(
Array(FEATURES.length).fill(false),
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setVisibleFeatures((prev) => {
const newVisible = [...prev];
newVisible[index] = true;
return newVisible;
});
}
});
},
{ threshold: 0.1 },
);
const elements = document.querySelectorAll(".feature-card");
elements.forEach((el) => {
observer.observe(el);
});
return () => observer.disconnect();
}, []);
return (
<Container size="lg" py={rem(80)}>
<Stack gap="xl" align="center" mb={rem(50)}>
<Transition
mounted={true}
transition="fade"
duration={600}
timingFunction="ease"
>
{(styles) => (
<div style={styles}>
<Title order={2} ta="center">
Everything You Need
</Title>
<Text c="dimmed" size="lg" ta="center" maw={600}>
A complete toolkit for building modern web applications with
best practices built-in.
</Text>
</div>
)}
</Transition>
</Stack>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
{FEATURES.map((feature, index) => (
<AnimatedFeatureCard
key={feature.title}
feature={feature}
index={index}
isVisible={visibleFeatures[index]}
/>
))}
</SimpleGrid>
</Container>
);
}
function AnimatedTestimonialCard({
testimonial,
index,
isVisible,
}: {
testimonial: (typeof TESTIMONIALS)[number];
index: number;
isVisible: boolean;
}) {
const [isDelayedVisible, setIsDelayedVisible] = useState(isVisible);
useEffect(() => {
if (isVisible) {
const timer = setTimeout(() => {
setIsDelayedVisible(true);
}, index * 150);
return () => clearTimeout(timer);
}
}, [isVisible, index]);
return (
<Transition
mounted={isDelayedVisible}
transition="slide-up"
duration={500}
timingFunction="ease"
>
{(styles) => (
<Card
padding="lg"
radius="md"
withBorder
shadow="sm"
className="testimonial-card"
style={styles}
>
<Text c="dimmed" mb="md">
"{testimonial.content}"
</Text>
<Group>
<Avatar src={testimonial.avatar} size="md" radius="xl" />
<Stack gap={0}>
<Text fw={600}>{testimonial.name}</Text>
<Text size="sm" c="dimmed">
{testimonial.role}
</Text>
</Stack>
</Group>
</Card>
)}
</Transition>
);
}
function TestimonialsSection() {
const [visibleTestimonials, setVisibleTestimonials] = useState(
Array(TESTIMONIALS.length).fill(false),
);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, index) => {
if (entry.isIntersecting) {
setVisibleTestimonials((prev) => {
const newVisible = [...prev];
newVisible[index] = true;
return newVisible;
});
}
});
},
{ threshold: 0.1 },
);
const elements = document.querySelectorAll(".testimonial-card");
elements.forEach((el) => {
observer.observe(el);
});
return () => observer.disconnect();
}, []);
return (
<Box py={rem(80)}>
<Container size="lg">
<Stack gap="xl" align="center" mb={rem(50)}>
<Transition
mounted={true}
transition="fade"
duration={600}
timingFunction="ease"
>
{(styles) => (
<div style={styles}>
<Title order={2} ta="center">
Loved by Developers
</Title>
<Text c="dimmed" size="lg" ta="center" maw={600}>
Join thousands of satisfied developers who have accelerated
their projects with Bun Stack.
</Text>
</div>
)}
</Transition>
</Stack>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
{TESTIMONIALS.map((testimonial, index) => (
<AnimatedTestimonialCard
key={testimonial.id}
testimonial={testimonial}
index={index}
isVisible={visibleTestimonials[index]}
/>
))}
</SimpleGrid>
</Container>
</Box>
);
}
function CtaSection() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
setLoaded(true);
}, []);
return (
<Container size="lg" py={rem(80)}>
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Paper
radius="lg"
p={rem(60)}
bg="blue"
style={{
...styles,
background:
"linear-gradient(135deg, var(--mantine-color-blue-6), var(--mantine-color-indigo-6))",
}}
>
<Stack align="center" gap="xl" ta="center">
<Title c="white" order={2}>
Ready to get started?
</Title>
<Text c="white" size="lg" maw={600}>
Join thousands of developers who are building faster and more
reliable applications with Bun Stack.
</Text>
<Group>
<Button
component={Link}
to="/signup"
size="lg"
variant="white"
color="dark"
rightSection={<IconChevronRight size="1.125rem" />}
>
Create Account
</Button>
<Button
component={Link}
to="/docs"
size="lg"
variant="outline"
color="white"
>
View Documentation
</Button>
</Group>
</Stack>
</Paper>
)}
</Transition>
</Container>
);
}
function Footer() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setLoaded(true);
}, 300);
return () => clearTimeout(timer);
}, []);
return (
<Transition
mounted={loaded}
transition="slide-up"
duration={600}
timingFunction="ease"
>
{(styles) => (
<Box
py={rem(40)}
style={{
...styles,
borderTop: "1px solid var(--mantine-color-gray-2)",
}}
>
<Container size="lg">
<Grid gutter={{ base: rem(40), md: rem(80) }}>
<Grid.Col span={{ base: 12, md: 4 }}>
<Stack gap="md">
<Title order={3}>BunStack</Title>
<Text size="sm" c="dimmed">
The ultimate full-stack solution for modern web
applications.
</Text>
<Group>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandGithub size="1.25rem" />
</ActionIcon>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandTwitter size="1.25rem" />
</ActionIcon>
<ActionIcon size="lg" variant="subtle" color="gray">
<IconBrandLinkedin size="1.25rem" />
</ActionIcon>
</Group>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Stack gap="xs">
<Title order={4}>Product</Title>
<Text
size="sm"
c="dimmed"
component={Link}
to="/features"
td="none"
>
Features
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/pricing"
td="none"
>
Pricing
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/docs"
td="none"
>
Documentation
</Text>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 2 }}>
<Stack gap="xs">
<Title order={4}>Company</Title>
<Text
size="sm"
c="dimmed"
component={Link}
to="/about"
td="none"
>
About
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/blog"
td="none"
>
Blog
</Text>
<Text
size="sm"
c="dimmed"
component={Link}
to="/careers"
td="none"
>
Careers
</Text>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 4 }}>
<Stack gap="xs">
<Title order={4}>Subscribe to our newsletter</Title>
<Text size="sm" c="dimmed">
Get the latest news and updates
</Text>
<Group>
<input
type="email"
placeholder="Your email"
style={{
padding: "8px 12px",
borderRadius: "4px",
border: "1px solid var(--mantine-color-gray-3)",
flex: 1,
}}
/>
<Button>Subscribe</Button>
</Group>
</Stack>
</Grid.Col>
</Grid>
<Box
pt={rem(40)}
style={{ borderTop: "1px solid var(--mantine-color-gray-2)" }}
>
<Group justify="space-between" align="center">
<Text size="sm" c="dimmed">
© 2024 Bun Stack. Built with Bun, Elysia, and React.
</Text>
<Group gap="lg">
<Text
component={Link}
to="/privacy"
size="sm"
c="dimmed"
style={{ textDecoration: "none" }}
>
Privacy Policy
</Text>
<Text
component={Link}
to="/terms"
size="sm"
c="dimmed"
style={{ textDecoration: "none" }}
>
Terms of Service
</Text>
</Group>
</Group>
</Box>
</Container>
</Box>
)}
</Transition>
);
}
function HomePage() {
return (
<Box>
<NavigationBar />
<HeroSection />
<FeaturesSection />
<TestimonialsSection />
<CtaSection />
<Footer />
</Box>
);
}

View File

@@ -2,11 +2,7 @@ import createClient from "openapi-fetch";
import type { paths } from "../../generated/api"; import type { paths } from "../../generated/api";
import { VITE_PUBLIC_URL } from "./env"; import { VITE_PUBLIC_URL } from "./env";
const baseUrl = const baseUrl = VITE_PUBLIC_URL;
VITE_PUBLIC_URL ||
(typeof window !== "undefined"
? window.location.origin
: "http://localhost:3000");
export const apiClient = createClient<paths>({ export const apiClient = createClient<paths>({
baseUrl: baseUrl, baseUrl: baseUrl,

View File

@@ -1,16 +1,11 @@
import { betterAuth } from "better-auth"; import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma"; import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "../../generated/prisma"; import { PrismaClient } from "../../generated/prisma";
import logger from "./logger"; import { VITE_PUBLIC_URL } from "./env";
const baseUrl = process.env.VITE_PUBLIC_URL; const baseUrl = VITE_PUBLIC_URL;
const prisma = new PrismaClient(); const prisma = new PrismaClient();
if (!baseUrl) {
logger.error("VITE_PUBLIC_URL is not defined");
throw new Error("VITE_PUBLIC_URL is not defined");
}
// logger.info('Initializing Better Auth with Prisma adapter'); // logger.info('Initializing Better Auth with Prisma adapter');
export const auth = betterAuth({ export const auth = betterAuth({
baseURL: baseUrl, baseURL: baseUrl,
@@ -26,6 +21,7 @@ export const auth = betterAuth({
clientId: process.env.GITHUB_CLIENT_ID || "CLIENT_ID_MISSING", clientId: process.env.GITHUB_CLIENT_ID || "CLIENT_ID_MISSING",
clientSecret: process.env.GITHUB_CLIENT_SECRET || "CLIENT_SECRET_MISSING", clientSecret: process.env.GITHUB_CLIENT_SECRET || "CLIENT_SECRET_MISSING",
enabled: true, enabled: true,
redirectURI: `${baseUrl}/api/auth/callback/github`,
}, },
}, },
user: { user: {
@@ -37,8 +33,24 @@ export const auth = betterAuth({
}, },
}, },
}, },
databaseHooks: {
user: {
create: {
before: async (user) => {
if (user.email === process.env.ADMIN_EMAIL) {
return {
data: {
...user,
role: "admin",
},
};
}
return { data: user };
},
},
},
},
secret: process.env.BETTER_AUTH_SECRET, secret: process.env.BETTER_AUTH_SECRET,
trustedOrigins: ["http://localhost:5173", "http://localhost:3000"],
session: { session: {
cookieCache: { cookieCache: {
enabled: true, enabled: true,
@@ -48,5 +60,6 @@ export const auth = betterAuth({
}, },
advanced: { advanced: {
cookiePrefix: "bun-react", cookiePrefix: "bun-react",
trustProxy: true,
}, },
}); });

View File

@@ -20,10 +20,21 @@ export const getEnv = (key: string, defaultValue = ""): string => {
return defaultValue; return defaultValue;
}; };
export const VITE_PUBLIC_URL = getEnv( export const VITE_PUBLIC_URL = (() => {
"VITE_PUBLIC_URL", // Priority:
"http://localhost:3000", // 1. BETTER_AUTH_URL (standard for better-auth)
); // 2. VITE_PUBLIC_URL (our app standard)
// 3. window.location.origin (browser fallback)
const envUrl = getEnv("BETTER_AUTH_URL") || getEnv("VITE_PUBLIC_URL");
if (envUrl) return envUrl;
// Fallback for browser
if (typeof window !== "undefined") {
return window.location.origin;
}
return "http://localhost:3000";
})();
export const IS_DEV = (() => { export const IS_DEV = (() => {
try { try {

View File

@@ -8,6 +8,7 @@ import { createServer as createViteServer } from "vite";
export async function createVite() { export async function createVite() {
return createViteServer({ return createViteServer({
root: process.cwd(), root: process.cwd(),
publicDir: "public",
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(process.cwd(), "./src"), "@": path.resolve(process.cwd(), "./src"),

View File

@@ -1,11 +1,15 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: [
"./src/index.html",
"./public/**/*.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: { theme: {
extend: { extend: {
colors: { colors: {
"darmasaba-navy": { "darmasaba-navy": {
DEFAULT: "#1E3A5F", // Primary navy color DEFAULT: "#1E3A5F",
50: "#E1E4F2", 50: "#E1E4F2",
100: "#B9C2DD", 100: "#B9C2DD",
200: "#91A0C9", 200: "#91A0C9",
@@ -18,7 +22,7 @@ module.exports = {
900: "#071833", 900: "#071833",
}, },
"darmasaba-blue": { "darmasaba-blue": {
DEFAULT: "#3B82F6", // Primary blue color DEFAULT: "#3B82F6",
50: "#E3F0FF", 50: "#E3F0FF",
100: "#B6D9FF", 100: "#B6D9FF",
200: "#89C2FF", 200: "#89C2FF",