Compare commits
31 Commits
main
...
nico/17-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d68d4dc06 | |||
| 97e6caa332 | |||
| f0c37272b9 | |||
| 8c35d58b38 | |||
| 952f7ecb16 | |||
| a74e0c02e5 | |||
| 17ecd3feca | |||
| d88cf2b100 | |||
| e0955ed2c4 | |||
| 918399bf62 | |||
| 7ce2eb6ae8 | |||
| 40772859f9 | |||
| c7b34b8c28 | |||
| 9bf73a305c | |||
| 947adc1537 | |||
| 9086e28961 | |||
| 66d207c081 | |||
| b77f6e8fa3 | |||
| 9e6734d1a5 | |||
|
|
1b9ddf0f4b | ||
| a0f440f6b3 | |||
| 1f56dd7660 | |||
| 1a2a213d0a | |||
| 1ec10fe623 | |||
| 226b0880e6 | |||
| 5d9be8c479 | |||
| e83bea2bc2 | |||
| 95c08681a7 | |||
| 9b015ec84d | |||
| 38b22dd2dd | |||
| 5801eb4596 |
19
.env.example
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -16,6 +16,7 @@ _.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
# Only .env.example is allowed to be committed
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
@@ -33,6 +34,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
# Dashboard-MD
|
||||
Dashboard-MD
|
||||
|
||||
# Playwright artifacts
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
62
Dockerfile
Normal 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"]
|
||||
139
Kinerja-Divisi-New.md
Normal file
@@ -0,0 +1,139 @@
|
||||
Create a modern admin dashboard UI for a village management system using React 19 + Vite + TailwindCSS + Mantine components + Recharts.
|
||||
|
||||
Design style:
|
||||
- Clean, soft UI with rounded corners (2xl)
|
||||
- Light gray background (#f5f6f8)
|
||||
- Card-based layout with subtle shadow
|
||||
- Smooth spacing and consistent padding
|
||||
- Professional government-style but still modern
|
||||
- Use Inter or system font
|
||||
- Primary color: dark blue
|
||||
- Accent color: orange for progress
|
||||
- Success color: green
|
||||
- Use Mantine components where possible
|
||||
|
||||
Layout:
|
||||
- Responsive grid layout (desktop-first)
|
||||
- 4 summary cards on top (horizontal)
|
||||
- 2 columns main content below
|
||||
- Left sidebar for division list
|
||||
- Right content for charts and activity
|
||||
|
||||
---
|
||||
|
||||
## 🔹 TOP CARDS (4 ITEMS)
|
||||
Each card contains:
|
||||
- Title (e.g: "Rakor 2025")
|
||||
- Progress bar (orange)
|
||||
- Date (small text)
|
||||
- Status badge "Selesai" (green)
|
||||
|
||||
Use:
|
||||
- Mantine Card
|
||||
- Mantine Progress
|
||||
- Mantine Badge
|
||||
|
||||
---
|
||||
|
||||
## 🔹 LEFT PANEL - "Divisi teraktif"
|
||||
Vertical list of divisions:
|
||||
- Each item is clickable
|
||||
- Show division name + number of activities
|
||||
- Rounded container with hover effect
|
||||
- Chevron icon on right
|
||||
|
||||
Example items:
|
||||
- Kesejahteraan (37 kegiatan)
|
||||
- Pemerintahan (26 kegiatan)
|
||||
- Keuangan (17 kegiatan)
|
||||
- etc
|
||||
|
||||
Use:
|
||||
- Scrollable container
|
||||
- Soft border + hover highlight
|
||||
|
||||
---
|
||||
|
||||
## 🔹 CENTER - BAR CHART (Jumlah Dokumen)
|
||||
- Use Recharts
|
||||
- Two bars:
|
||||
- Gambar
|
||||
- Dokumen
|
||||
- Color:
|
||||
- Yellow/orange
|
||||
- Green
|
||||
- Show Y axis scale (0–400)
|
||||
|
||||
---
|
||||
|
||||
## 🔹 RIGHT - PIE CHART (Progres Kegiatan)
|
||||
- Use Recharts PieChart
|
||||
- Segments:
|
||||
- Selesai (green ~83%)
|
||||
- Dikerjakan (orange ~16%)
|
||||
- Segera dikerjakan (blue)
|
||||
- Dibatalkan (red)
|
||||
- Include legend below
|
||||
|
||||
---
|
||||
|
||||
## 🔹 BOTTOM LEFT - Diskusi Panel
|
||||
- List of discussion messages
|
||||
- Each item:
|
||||
- Title
|
||||
- Sender name
|
||||
- Date
|
||||
- Styled like notification cards
|
||||
- Compact and clean
|
||||
|
||||
---
|
||||
|
||||
## 🔹 BOTTOM RIGHT - "Acara Hari Ini"
|
||||
- Empty state
|
||||
- Show text: "Tidak ada acara hari ini"
|
||||
- Centered, muted text
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ TECH REQUIREMENTS
|
||||
|
||||
- Use React functional components
|
||||
- Use TanStack Router (file-based or route config)
|
||||
- Use Mantine for UI components
|
||||
- Use Tailwind for layout and spacing
|
||||
- Use Recharts for charts
|
||||
- State management: Valtio (simple global state)
|
||||
- Date formatting: dayjs
|
||||
- Icons: lucide-react
|
||||
|
||||
---
|
||||
|
||||
## 📁 COMPONENT STRUCTURE
|
||||
|
||||
- components/
|
||||
- DashboardCard.tsx
|
||||
- DivisionList.tsx
|
||||
- BarChartCard.tsx
|
||||
- PieChartCard.tsx
|
||||
- DiscussionList.tsx
|
||||
- EmptyState.tsx
|
||||
|
||||
- routes/
|
||||
- dashboard.tsx
|
||||
|
||||
---
|
||||
|
||||
## ✨ EXTRA (IMPORTANT FOR VIBE CODING)
|
||||
|
||||
- Add subtle hover animations (scale 1.02)
|
||||
- Smooth transitions (150–200ms)
|
||||
- Keep spacing consistent (gap-4 / gap-6)
|
||||
- Avoid clutter, prioritize readability
|
||||
- Make it feel "calm and productive"
|
||||
|
||||
---
|
||||
|
||||
Output:
|
||||
- Full React component code (modular, not monolithic)
|
||||
- Clean, readable, production-ready
|
||||
- No unnecessary comments
|
||||
302
PromptDashboard.md
Normal file
@@ -0,0 +1,302 @@
|
||||
Buat halaman dashboard admin modern untuk sistem pemerintahan desa bernama **Darmasaba Dashboard NOC**.
|
||||
|
||||
Gunakan stack berikut:
|
||||
|
||||
Frontend:
|
||||
|
||||
* React 19
|
||||
* Bun runtime
|
||||
* Vite
|
||||
* TailwindCSS
|
||||
* Mantine UI
|
||||
* Mantine Charts atau Recharts
|
||||
* Tabler Icons
|
||||
* TanStack Router
|
||||
* Dayjs
|
||||
|
||||
UI harus modular dengan reusable components.
|
||||
|
||||
Gunakan **TailwindCSS sebagai styling utama** dengan warna dari konfigurasi berikut:
|
||||
|
||||
Primary:
|
||||
darmasaba-navy (#1E3A5F)
|
||||
|
||||
Secondary:
|
||||
darmasaba-blue (#3B82F6)
|
||||
|
||||
Success:
|
||||
#22C55E
|
||||
|
||||
Warning:
|
||||
#FACC15
|
||||
|
||||
Danger:
|
||||
#EF4444
|
||||
|
||||
Background:
|
||||
#F5F8FB
|
||||
|
||||
Dashboard harus memiliki **Light Mode dan Dark Mode**.
|
||||
|
||||
Dark Mode Color Rules:
|
||||
background: #0F172A
|
||||
card: #1E293B
|
||||
border: #334155
|
||||
text: #E2E8F0
|
||||
|
||||
Card style:
|
||||
|
||||
* rounded-xl
|
||||
* soft shadow
|
||||
* padding besar
|
||||
* border subtle
|
||||
* smooth hover animation
|
||||
|
||||
Gunakan grid layout responsive.
|
||||
|
||||
---
|
||||
|
||||
SECTION 1 — PROGRAM KEGIATAN
|
||||
|
||||
Buat 4 card horizontal di bagian atas yang menampilkan kegiatan desa.
|
||||
|
||||
Setiap card memiliki:
|
||||
|
||||
* header biru
|
||||
* progress bar kegiatan
|
||||
* tanggal kegiatan
|
||||
* badge status
|
||||
|
||||
Data card:
|
||||
|
||||
1.
|
||||
|
||||
Judul: Rakor 2025
|
||||
Tanggal: 3 Juli 2025
|
||||
Progress: 90%
|
||||
Status: selesai
|
||||
|
||||
2.
|
||||
|
||||
Judul: Pemutakhiran Indeks Desa
|
||||
Tanggal: 3 Juli 2025
|
||||
Progress: 85%
|
||||
Status: selesai
|
||||
|
||||
3.
|
||||
|
||||
Judul: Mengurus Akta Cerai Warga
|
||||
Tanggal: 3 Juli 2025
|
||||
Progress: 80%
|
||||
Status: selesai
|
||||
|
||||
4.
|
||||
|
||||
Judul: Pasek 7 Desa Adat
|
||||
Tanggal: 3 Juli 2025
|
||||
Progress: 92%
|
||||
Status: selesai
|
||||
|
||||
Progress bar:
|
||||
|
||||
* rounded
|
||||
* warna warning
|
||||
* animasi smooth
|
||||
|
||||
Status badge:
|
||||
|
||||
* success color
|
||||
|
||||
---
|
||||
|
||||
SECTION 2 — GRID DASHBOARD
|
||||
|
||||
Layout:
|
||||
|
||||
3 column grid.
|
||||
|
||||
Left column (sidebar style):
|
||||
Divisi Teraktif
|
||||
|
||||
List item card dengan arrow icon.
|
||||
|
||||
Data:
|
||||
|
||||
Kesejahteraan — 37 kegiatan
|
||||
Pemerintahan — 26 kegiatan
|
||||
Keuangan — 17 kegiatan
|
||||
Sekretaris Desa — 15 kegiatan
|
||||
Tata Usaha TK — 14 kegiatan
|
||||
Perangkat Kewilayahan — 12 kegiatan
|
||||
Pelayanan — 10 kegiatan
|
||||
Perencanaan — 9 kegiatan
|
||||
Tata Usaha & Umum — 7 kegiatan
|
||||
|
||||
Setiap item:
|
||||
|
||||
* rounded
|
||||
* hover effect
|
||||
* arrow icon kanan
|
||||
|
||||
---
|
||||
|
||||
Middle column:
|
||||
|
||||
Jumlah Dokumen
|
||||
|
||||
Gunakan **Bar Chart**.
|
||||
|
||||
Kategori:
|
||||
|
||||
* Gambar
|
||||
* Dokumen
|
||||
|
||||
Nilai:
|
||||
|
||||
* Gambar: 300
|
||||
* Dokumen: 310
|
||||
|
||||
Gunakan:
|
||||
Recharts atau Mantine Charts.
|
||||
|
||||
---
|
||||
|
||||
Right column:
|
||||
|
||||
Progres Kegiatan
|
||||
|
||||
Gunakan **Pie Chart**.
|
||||
|
||||
Data:
|
||||
|
||||
Selesai — 83.33%
|
||||
Dikerjakan — 16.67%
|
||||
Segera Dikerjakan — 0%
|
||||
Dibatalkan — 0%
|
||||
|
||||
Legend harus berwarna.
|
||||
|
||||
---
|
||||
|
||||
SECTION 3 — DISCUSSION PANEL
|
||||
|
||||
Judul: Diskusi
|
||||
|
||||
Tampilkan list diskusi internal staf.
|
||||
|
||||
Item card memiliki:
|
||||
|
||||
* icon chat
|
||||
* judul pesan
|
||||
* nama pengirim
|
||||
* tanggal
|
||||
|
||||
Contoh data:
|
||||
|
||||
"Kepada Pelayanan, mohon di cek..."
|
||||
Pengirim: I.B Surya Prabhawa Manu
|
||||
Tanggal: 12 Apr 2025
|
||||
|
||||
"Kepada staf perencanaan @suar..."
|
||||
Pengirim: Ni Nyoman Yuliani
|
||||
Tanggal: 14 Jun 2025
|
||||
|
||||
"ijin atau mohon kepada KBD sar..."
|
||||
Pengirim: Ni Wayan Martini
|
||||
Tanggal: 12 Apr 2025
|
||||
|
||||
---
|
||||
|
||||
SECTION 4 — ACARA HARI INI
|
||||
|
||||
Card sederhana.
|
||||
|
||||
Jika tidak ada acara tampilkan:
|
||||
|
||||
"Tidak ada acara hari ini"
|
||||
|
||||
---
|
||||
|
||||
SECTION 5 — ARSIP DIGITAL PERANGKAT DESA
|
||||
|
||||
Grid 2 column.
|
||||
|
||||
Menu arsip:
|
||||
|
||||
Surat Keputusan
|
||||
Dokumentasi
|
||||
Laporan Keuangan
|
||||
Notulensi Rapat
|
||||
|
||||
Setiap item berupa card clickable dengan:
|
||||
|
||||
* icon dokumen
|
||||
* border
|
||||
* hover effect
|
||||
|
||||
---
|
||||
|
||||
DESIGN STYLE
|
||||
|
||||
Gunakan gaya:
|
||||
|
||||
Modern Government Dashboard
|
||||
Clean UI
|
||||
Soft shadow
|
||||
Rounded-xl
|
||||
Spacing besar
|
||||
Minimalistic
|
||||
|
||||
---
|
||||
|
||||
RESPONSIVE RULES
|
||||
|
||||
Desktop:
|
||||
12 column grid
|
||||
|
||||
Tablet:
|
||||
6 column grid
|
||||
|
||||
Mobile:
|
||||
single column stack
|
||||
|
||||
---
|
||||
|
||||
COMPONENT STRUCTURE
|
||||
|
||||
src/components/dashboard
|
||||
|
||||
activity-card.tsx
|
||||
division-list.tsx
|
||||
document-chart.tsx
|
||||
progress-chart.tsx
|
||||
discussion-panel.tsx
|
||||
event-card.tsx
|
||||
archive-card.tsx
|
||||
|
||||
src/pages
|
||||
|
||||
dashboard.tsx
|
||||
|
||||
---
|
||||
|
||||
CODE QUALITY
|
||||
|
||||
Gunakan:
|
||||
|
||||
* React hooks
|
||||
* reusable components
|
||||
* Mantine components jika perlu
|
||||
* Tailwind utility classes
|
||||
* dark mode support
|
||||
* responsive layout
|
||||
* clean TypeScript
|
||||
|
||||
---
|
||||
|
||||
Output:
|
||||
|
||||
* Halaman dashboard lengkap
|
||||
* Semua komponen reusable
|
||||
* Chart sudah bekerja
|
||||
* Layout identik dengan desain dashboard modern pemerintahan
|
||||
12
QWEN.md
@@ -51,11 +51,13 @@ bun install
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Fill in your DATABASE_URL and BETTER_AUTH_SECRET
|
||||
# Optional: Set ADMIN_EMAIL and ADMIN_PASSWORD for admin user
|
||||
```
|
||||
|
||||
### Database Initialization
|
||||
```bash
|
||||
bun x prisma migrate dev
|
||||
bun run seed
|
||||
```
|
||||
|
||||
### Start Development
|
||||
@@ -109,4 +111,12 @@ bun run dev
|
||||
- `test:e2e`: Runs end-to-end tests
|
||||
- `build`: Builds the application for production
|
||||
- `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)
|
||||
BIN
Screenshot 2026-03-10 at 16.48.25.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
6
bun.lock
@@ -47,6 +47,7 @@
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-router": "^1.158.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-auth": "^1.4.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cmdk": "^1.0.1",
|
||||
@@ -80,6 +81,7 @@
|
||||
"@tanstack/react-router-devtools": "^1.158.1",
|
||||
"@tanstack/router-cli": "1.158.1",
|
||||
"@tanstack/router-vite-plugin": "^1.158.1",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bun": "latest",
|
||||
"@types/react": "^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/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/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=="],
|
||||
|
||||
"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-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=="],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"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 .",
|
||||
"check": "biome check --write .",
|
||||
"format": "biome format --write .",
|
||||
@@ -12,7 +12,7 @@
|
||||
"test": "bun test __tests__/api",
|
||||
"test:ui": "bun test --ui __tests__/api",
|
||||
"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 build ./src/index.html --outdir=dist --sourcemap --target=browser --minify --define:process.env.NODE_ENV='\"production\"' --env='VITE_*' && cp -r public/* dist/ 2>/dev/null || true",
|
||||
"start": "NODE_ENV=production bun src/index.ts",
|
||||
"seed": "bun prisma/seed.ts"
|
||||
},
|
||||
@@ -59,6 +59,7 @@
|
||||
"@tabler/icons-react": "^3.36.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-router": "^1.158.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-auth": "^1.4.18",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"cmdk": "^1.0.1",
|
||||
@@ -92,6 +93,7 @@
|
||||
"@tanstack/react-router-devtools": "^1.158.1",
|
||||
"@tanstack/router-cli": "1.158.1",
|
||||
"@tanstack/router-vite-plugin": "^1.158.1",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bun": "latest",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
112
prisma/seed.ts
@@ -1,13 +1,16 @@
|
||||
import "dotenv/config";
|
||||
import { hash } from "bcryptjs";
|
||||
import { generateId } from "better-auth";
|
||||
import { prisma } from "@/utils/db";
|
||||
|
||||
async function seedAdminUser() {
|
||||
// Load environment variables
|
||||
const adminEmail = process.env.ADMIN_EMAIL;
|
||||
const adminPassword = process.env.ADMIN_PASSWORD || "admin123";
|
||||
|
||||
if (!adminEmail) {
|
||||
console.log(
|
||||
"No ADMIN_EMAIL environment variable found. Skipping admin role assignment.",
|
||||
"No ADMIN_EMAIL environment variable found. Skipping admin user creation.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -30,9 +33,35 @@ async function seedAdminUser() {
|
||||
console.log(`User with email ${adminEmail} already has admin role.`);
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`No user found with email ${adminEmail}. Skipping admin role assignment.`,
|
||||
);
|
||||
// Create new admin user
|
||||
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) {
|
||||
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() {
|
||||
console.log("Seeding database...");
|
||||
|
||||
await seedAdminUser();
|
||||
await seedDemoUsers();
|
||||
|
||||
console.log("Database seeding completed.");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Error during seeding:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
// Only auto-execute when run directly (not when imported)
|
||||
const isMainModule =
|
||||
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
|
After Width: | Height: | Size: 54 KiB |
BIN
public/SDGS-16.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/SDGS-3.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
public/SDGS-7.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
public/logo-desa-plus.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
58
scripts/build.ts
Normal 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!");
|
||||
@@ -1,296 +1,389 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
Button,
|
||||
Badge,
|
||||
Table,
|
||||
Stack,
|
||||
Select,
|
||||
useMantineColorScheme
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconBuildingStore, IconCategory, IconCurrency, IconUsers } from "@tabler/icons-react";
|
||||
import {
|
||||
IconBuildingStore,
|
||||
IconCategory,
|
||||
IconCurrency,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const BumdesPage = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
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",
|
||||
},
|
||||
];
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
// Sample data for top products
|
||||
const topProducts = [
|
||||
{
|
||||
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%",
|
||||
},
|
||||
];
|
||||
const [timeFilter, setTimeFilter] = useState<string>("bulan");
|
||||
|
||||
// Sample data for product sales
|
||||
const productSales = [
|
||||
{
|
||||
produk: "Beras Premium Organik",
|
||||
penjualanBulanIni: "Rp 8.500.000",
|
||||
bulanLalu: "Rp 8.500.000",
|
||||
trend: 10,
|
||||
volume: "650 Kg",
|
||||
stok: "850 Kg",
|
||||
},
|
||||
{
|
||||
produk: "Keripik Singkong",
|
||||
penjualanBulanIni: "Rp 4.200.000",
|
||||
bulanLalu: "Rp 3.800.000",
|
||||
trend: 10,
|
||||
volume: "320 Kg",
|
||||
stok: "120 Kg",
|
||||
},
|
||||
{
|
||||
produk: "Madu Alami",
|
||||
penjualanBulanIni: "Rp 3.750.000",
|
||||
bulanLalu: "Rp 4.100.000",
|
||||
trend: -8,
|
||||
volume: "150 Liter",
|
||||
stok: "45 Liter",
|
||||
},
|
||||
{
|
||||
produk: "Kecap Tradisional",
|
||||
penjualanBulanIni: "Rp 2.800.000",
|
||||
bulanLalu: "Rp 2.500.000",
|
||||
trend: 12,
|
||||
volume: "280 Botol",
|
||||
stok: "95 Botol",
|
||||
},
|
||||
];
|
||||
// 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",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* KPI Cards */}
|
||||
<Grid gutter="md">
|
||||
{kpiData.map((kpi, index) => (
|
||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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"}>
|
||||
{kpi.title}
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{typeof kpi.value === 'number' ? kpi.value.toLocaleString() : kpi.value}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={kpi.color}
|
||||
p={8}
|
||||
radius="md"
|
||||
>
|
||||
{kpi.icon}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
// Sample data for top products
|
||||
const topProducts = [
|
||||
{
|
||||
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%",
|
||||
},
|
||||
];
|
||||
|
||||
{/* Update Penjualan Produk Header */}
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Group justify="space-between" align="center" px="md" py="xs">
|
||||
<Title order={3} c={dark ? "dark.0" : "black"}>
|
||||
Update Penjualan Produk
|
||||
</Title>
|
||||
<Group>
|
||||
<Button
|
||||
variant={timeFilter === "minggu" ? "filled" : "light"}
|
||||
onClick={() => setTimeFilter("minggu")}
|
||||
color="darmasaba-blue"
|
||||
>
|
||||
Minggu ini
|
||||
</Button>
|
||||
<Button
|
||||
variant={timeFilter === "bulan" ? "filled" : "light"}
|
||||
onClick={() => setTimeFilter("bulan")}
|
||||
color="darmasaba-blue"
|
||||
>
|
||||
Bulan ini
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
// Sample data for product sales
|
||||
const productSales = [
|
||||
{
|
||||
produk: "Beras Premium Organik",
|
||||
penjualanBulanIni: "Rp 8.500.000",
|
||||
bulanLalu: "Rp 8.500.000",
|
||||
trend: 10,
|
||||
volume: "650 Kg",
|
||||
stok: "850 Kg",
|
||||
},
|
||||
{
|
||||
produk: "Keripik Singkong",
|
||||
penjualanBulanIni: "Rp 4.200.000",
|
||||
bulanLalu: "Rp 3.800.000",
|
||||
trend: 10,
|
||||
volume: "320 Kg",
|
||||
stok: "120 Kg",
|
||||
},
|
||||
{
|
||||
produk: "Madu Alami",
|
||||
penjualanBulanIni: "Rp 3.750.000",
|
||||
bulanLalu: "Rp 4.100.000",
|
||||
trend: -8,
|
||||
volume: "150 Liter",
|
||||
stok: "45 Liter",
|
||||
},
|
||||
{
|
||||
produk: "Kecap Tradisional",
|
||||
penjualanBulanIni: "Rp 2.800.000",
|
||||
bulanLalu: "Rp 2.500.000",
|
||||
trend: 12,
|
||||
volume: "280 Botol",
|
||||
stok: "95 Botol",
|
||||
},
|
||||
];
|
||||
|
||||
<Grid gutter="md">
|
||||
{/* Produk Unggulan (Left Column) */}
|
||||
<GridCol span={{ base: 12, lg: 4 }}>
|
||||
<Stack gap="md">
|
||||
{/* Total Penjualan, Produk Aktif, Total Transaksi */}
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Total Penjualan</Text>
|
||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>Rp 28.500.000</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Produk Aktif</Text>
|
||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>124 Produk</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>Total Transaksi</Text>
|
||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>1.240 Transaksi</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* KPI Cards */}
|
||||
<Grid gutter="md">
|
||||
{kpiData.map((kpi, index) => (
|
||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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"}>
|
||||
{kpi.title}
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{typeof kpi.value === "number"
|
||||
? kpi.value.toLocaleString()
|
||||
: kpi.value}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Badge variant="light" color={kpi.color} p={8} radius="md">
|
||||
{kpi.icon}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Top 3 Produk Terlaris */}
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>Top 3 Produk Terlaris</Title>
|
||||
<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>
|
||||
{/* Update Penjualan Produk Header */}
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Group justify="space-between" align="center" px="md" py="xs">
|
||||
<Title order={3} c={dark ? "dark.0" : "black"}>
|
||||
Update Penjualan Produk
|
||||
</Title>
|
||||
<Group>
|
||||
<Button
|
||||
variant={timeFilter === "minggu" ? "filled" : "light"}
|
||||
onClick={() => setTimeFilter("minggu")}
|
||||
color="darmasaba-blue"
|
||||
>
|
||||
Minggu ini
|
||||
</Button>
|
||||
<Button
|
||||
variant={timeFilter === "bulan" ? "filled" : "light"}
|
||||
onClick={() => setTimeFilter("bulan")}
|
||||
color="darmasaba-blue"
|
||||
>
|
||||
Bulan ini
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
|
||||
{/* Detail Penjualan Produk (Right Column) */}
|
||||
<GridCol span={{ base: 12, lg: 8 }}>
|
||||
<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>
|
||||
<Select
|
||||
placeholder="Filter kategori"
|
||||
data={[
|
||||
{ value: 'semua', label: 'Semua Kategori' },
|
||||
{ value: 'makanan', label: 'Makanan' },
|
||||
{ value: 'minuman', label: 'Minuman' },
|
||||
{ value: 'kerajinan', label: 'Kerajinan' },
|
||||
]}
|
||||
defaultValue="semua"
|
||||
w={200}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Table striped highlightOnHover withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th><Text c={dark ? "white" : "dimmed"}>Produk</Text></Table.Th>
|
||||
<Table.Th><Text c={dark ? "white" : "dimmed"}>Penjualan Bulan Ini</Text></Table.Th>
|
||||
<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>
|
||||
<Table.Th><Text c={dark ? "white" : "dimmed"}>Stok</Text></Table.Th>
|
||||
<Table.Th><Text c={dark ? "white" : "dimmed"}>Aksi</Text></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{productSales.map((product, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>{product.produk}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>{product.penjualanBulanIni}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={"sm"} c={dark ? "white" : "dimmed"}>{product.bulanLalu}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<Text c={product.trend >= 0 ? "green" : "red"}>
|
||||
{product.trend >= 0 ? '↑' : '↓'} {Math.abs(product.trend)}%
|
||||
</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>{product.volume}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={parseInt(product.stok) > 200 ? "green" : "yellow"}
|
||||
>
|
||||
{product.stok}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Button variant="subtle" size="compact-sm" color="darmasaba-blue">
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
<Grid gutter="md">
|
||||
{/* Produk Unggulan (Left Column) */}
|
||||
<GridCol span={{ base: 12, lg: 4 }}>
|
||||
<Stack gap="md">
|
||||
{/* Total Penjualan, Produk Aktif, Total Transaksi */}
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
Total Penjualan
|
||||
</Text>
|
||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
Rp 28.500.000
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
Produk Aktif
|
||||
</Text>
|
||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
124 Produk
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
Total Transaksi
|
||||
</Text>
|
||||
<Text size="lg" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
1.240 Transaksi
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Top 3 Produk Terlaris */}
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
|
||||
Top 3 Produk Terlaris
|
||||
</Title>
|
||||
<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) */}
|
||||
<GridCol span={{ base: 12, lg: 8 }}>
|
||||
<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>
|
||||
<Select
|
||||
placeholder="Filter kategori"
|
||||
data={[
|
||||
{ value: "semua", label: "Semua Kategori" },
|
||||
{ value: "makanan", label: "Makanan" },
|
||||
{ value: "minuman", label: "Minuman" },
|
||||
{ value: "kerajinan", label: "Kerajinan" },
|
||||
]}
|
||||
defaultValue="semua"
|
||||
w={200}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Table striped highlightOnHover withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dimmed"}>Produk</Text>
|
||||
</Table.Th>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dimmed"}>
|
||||
Penjualan Bulan Ini
|
||||
</Text>
|
||||
</Table.Th>
|
||||
<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>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dimmed"}>Stok</Text>
|
||||
</Table.Th>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dimmed"}>Aksi</Text>
|
||||
</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{productSales.map((product, index) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
{product.produk}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>
|
||||
{product.penjualanBulanIni}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={"sm"} c={dark ? "white" : "dimmed"}>
|
||||
{product.bulanLalu}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<Text c={product.trend >= 0 ? "green" : "red"}>
|
||||
{product.trend >= 0 ? "↑" : "↓"}{" "}
|
||||
{Math.abs(product.trend)}%
|
||||
</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={"sm"} c={dark ? "dark.0" : "black"}>
|
||||
{product.volume}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
variant="light"
|
||||
color={
|
||||
parseInt(product.stok) > 200 ? "green" : "yellow"
|
||||
}
|
||||
>
|
||||
{product.stok}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="compact-sm"
|
||||
color="darmasaba-blue"
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Card>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default BumdesPage;
|
||||
export default BumdesPage;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
// 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 { Card } from "./ui/card";
|
||||
|
||||
|
||||
@@ -1,408 +1,117 @@
|
||||
import {
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
FileText,
|
||||
MessageCircle,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart,
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip, // Added Tooltip import
|
||||
} from "recharts";
|
||||
import { Grid, Image, Stack, useMantineColorScheme } from "@mantine/core";
|
||||
import { CheckCircle, FileText, MessageCircle, Users } from "lucide-react";
|
||||
import { ActivityList } from "./dashboard/activity-list";
|
||||
import { ChartAPBDes } from "./dashboard/chart-apbdes";
|
||||
import { ChartSurat } from "./dashboard/chart-surat";
|
||||
import { DivisionProgress } from "./dashboard/division-progress";
|
||||
import { SatisfactionChart } from "./dashboard/satisfaction-chart";
|
||||
import { SDGSCard } from "./dashboard/sdgs-card";
|
||||
import { StatCard } from "./dashboard/stat-card";
|
||||
|
||||
// Import Mantine components
|
||||
|
||||
import {
|
||||
Grid,
|
||||
Stack,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
ActionIcon,
|
||||
Progress,
|
||||
Box,
|
||||
Badge,
|
||||
ThemeIcon,
|
||||
Card, // Added for icon containers
|
||||
useMantineColorScheme, // Add this import
|
||||
} from "@mantine/core";
|
||||
|
||||
const barChartData = [
|
||||
{ month: "Jan", value: 145 },
|
||||
{ month: "Feb", value: 165 },
|
||||
{ month: "Mar", value: 195 },
|
||||
{ month: "Apr", value: 155 },
|
||||
{ month: "Mei", value: 205 },
|
||||
{ month: "Jun", value: 185 },
|
||||
];
|
||||
|
||||
const pieChartData = [
|
||||
{ name: "Puas", value: 25 },
|
||||
{ name: "Cukup", value: 25 },
|
||||
{ name: "Kurang", value: 25 },
|
||||
{ name: "Sangat puas", value: 25 },
|
||||
];
|
||||
|
||||
const COLORS = ["#4E5BA6", "#F4C542", "#8CC63F", "#E57373"];
|
||||
|
||||
const divisiData = [
|
||||
{ name: "Kesejahteraan", value: 37 },
|
||||
{ name: "Pemerintahan", value: 26 },
|
||||
{ name: "Keuangan", value: 17 },
|
||||
{ name: "Sekretaris Desa", value: 15 },
|
||||
];
|
||||
|
||||
const eventData = [
|
||||
{ date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" },
|
||||
{ date: "15 Oktober 2025", title: "Davest" },
|
||||
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
|
||||
const sdgsData = [
|
||||
{
|
||||
title: "Desa Berenergi Bersih dan Terbarukan",
|
||||
score: 99.64,
|
||||
image: "SDGS-7.png",
|
||||
},
|
||||
{
|
||||
title: "Desa Damai Berkeadilan",
|
||||
score: 78.65,
|
||||
image: "SDGS-16.png",
|
||||
},
|
||||
{
|
||||
title: "Desa Sehat dan Sejahtera",
|
||||
score: 77.37,
|
||||
image: "SDGS-3.png",
|
||||
},
|
||||
{
|
||||
title: "Desa Tanpa Kemiskinan",
|
||||
score: 52.62,
|
||||
image: "SDGS-1.png",
|
||||
},
|
||||
];
|
||||
|
||||
export function DashboardContent() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Stats Cards */}
|
||||
{/* Header Metrics - 4 Stat Cards */}
|
||||
<Grid gutter="md">
|
||||
<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"}>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Surat Minggu Ini
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700}>
|
||||
99
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
14 baru, 14 diproses
|
||||
</Text>
|
||||
<Text size="sm" c="red" mt="xs">
|
||||
12% dari minggu lalu ↗ +12%
|
||||
</Text>
|
||||
</Box>
|
||||
<ThemeIcon variant="filled" size="xl" radius="xl" color={dark ? 'gray' : 'darmasaba-blue'}>
|
||||
<FileText style={{ width: "70%", height: "70%" }} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<StatCard
|
||||
title="Surat Minggu Ini"
|
||||
value={99}
|
||||
detail="14 baru, 14 diproses"
|
||||
trend="12% dari minggu lalu ↗ +12%"
|
||||
trendValue={12}
|
||||
icon={<FileText style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<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"}>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Pengaduan Aktif
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700}>
|
||||
28
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
14 baru, 14 diproses
|
||||
</Text>
|
||||
</Box>
|
||||
<ThemeIcon variant="filled" size="xl" radius="xl" color={dark ? 'gray' : 'darmasaba-blue'}>
|
||||
<MessageCircle style={{ width: "70%", height: "70%" }} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<StatCard
|
||||
title="Pengaduan Aktif"
|
||||
value={28}
|
||||
detail="14 baru, 14 diproses"
|
||||
icon={<MessageCircle style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<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"}>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Layanan Selesai
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700}>
|
||||
156
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
bulan ini
|
||||
</Text>
|
||||
<Text size="sm" c="red" mt="xs">
|
||||
+8%
|
||||
</Text>
|
||||
</Box>
|
||||
<ThemeIcon variant="filled" size="xl" radius="xl" color={dark ? 'gray' : 'darmasaba-blue'}>
|
||||
<CheckCircle style={{ width: "70%", height: "70%" }} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<StatCard
|
||||
title="Layanan Selesai"
|
||||
value={156}
|
||||
detail="bulan ini"
|
||||
trend="+8%"
|
||||
trendValue={8}
|
||||
icon={<CheckCircle style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<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"}>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
Kepuasan Warga
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700}>
|
||||
87.2%
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
dari 482 responden
|
||||
</Text>
|
||||
</Box>
|
||||
<ThemeIcon variant="filled" size="xl" radius="xl" color={dark ? 'gray' : 'darmasaba-blue'}>
|
||||
<Users style={{ width: "70%", height: "70%" }} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
<StatCard
|
||||
title="Kepuasan Warga"
|
||||
value="87.2%"
|
||||
detail="dari 482 responden"
|
||||
icon={<Users style={{ width: "70%", height: "70%" }} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Section 2: Chart & Division Progress */}
|
||||
<Grid gutter="lg">
|
||||
{/* Bar Chart */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Box>
|
||||
<Title order={4} mb={5}>
|
||||
Statistik Pengajuan Surat
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
Trend pengajuan surat 6 bulan terakhir
|
||||
</Text>
|
||||
</Box>
|
||||
<ActionIcon variant="subtle" size="lg" radius="md">
|
||||
{/* Original SVG converted to a generic Icon placeholder */}
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 5L13 10L8 15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={barChartData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke="var(--mantine-color-gray-3)"
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: "var(--mantine-color-text)" }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
ticks={[0, 55, 110, 165, 220]}
|
||||
tick={{ fill: "var(--mantine-color-text)" }}
|
||||
/>
|
||||
<Tooltip />
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--mantine-color-blue-filled)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||
<ChartSurat />
|
||||
</Grid.Col>
|
||||
|
||||
{/* Pie Chart */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
|
||||
<Title order={4} mb={5}>
|
||||
Tingkat Kepuasan
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Tingkat kepuasan layanan
|
||||
</Text>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieChartData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={80}
|
||||
outerRadius={120}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{pieChartData.map((_entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<Group justify="center" gap="md" mt="md">
|
||||
<Group gap="xs">
|
||||
<Box w={12} h={12} style={{ backgroundColor: COLORS[0], borderRadius: "50%" }} />
|
||||
<Text size="sm">Sangat puas (0%)</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Box w={12} h={12} style={{ backgroundColor: COLORS[1], borderRadius: "50%" }} />
|
||||
<Text size="sm">Puas (0%)</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Box w={12} h={12} style={{ backgroundColor: COLORS[2], borderRadius: "50%" }} />
|
||||
<Text size="sm">Cukup (0%)</Text>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Box w={12} h={12} style={{ backgroundColor: COLORS[3], borderRadius: "50%" }} />
|
||||
<Text size="sm">Kurang (0%)</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||
<SatisfactionChart />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Bottom Section */}
|
||||
{/* Section 3: APBDes Chart */}
|
||||
<Grid gutter="lg">
|
||||
{/* Divisi Teraktif */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
|
||||
<Group gap="xs" mb="lg">
|
||||
<Box>
|
||||
{/* Original SVG icon */}
|
||||
<svg
|
||||
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>
|
||||
<Title order={4}>Divisi Teraktif</Title>
|
||||
</Group>
|
||||
<Stack gap="sm">
|
||||
{divisiData.map((divisi, index) => (
|
||||
<Box key={index}>
|
||||
<Group justify="space-between" mb={5}>
|
||||
<Text size="sm" fw={500}>
|
||||
{divisi.name}
|
||||
</Text>
|
||||
<Text size="sm" fw={600}>
|
||||
{divisi.value} Kegiatan
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={(divisi.value / 37) * 100}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
color="blue"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||
<DivisionProgress />
|
||||
</Grid.Col>
|
||||
|
||||
{/* Kalender */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
|
||||
<Group gap="xs" mb="lg">
|
||||
<Calendar style={{ width: 20, height: 20 }} />
|
||||
<Title order={4}>Kalender & Kegiatan Mendatang</Title>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
{eventData.map((event, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
style={{ borderLeft: "4px solid var(--mantine-color-blue-filled)", paddingLeft: 12 }}
|
||||
>
|
||||
<Text size="sm" c="dimmed">
|
||||
{event.date}
|
||||
</Text>
|
||||
<Text fw={500}>{event.title}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||
<ActivityList />
|
||||
{/* <SatisfactionChart /> */}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* APBDes Chart */}
|
||||
<Card p="md" style={{borderColor: dark ? "#141D34" : "white"}} radius="md" withBorder bg={dark ? "#141D34" : "white"}>
|
||||
<Title order={4} mb="lg">
|
||||
Grafik APBDes
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
<Group align="center" gap="md">
|
||||
<Text size="sm" fw={500} w={60}>
|
||||
Belanja
|
||||
</Text>
|
||||
<Progress value={70} size="lg" radius="xl" color="blue" style={{ flex: 1 }} />
|
||||
</Group>
|
||||
<Group align="center" gap="md">
|
||||
<Text size="sm" fw={500} w={60}>
|
||||
Pendapatan
|
||||
</Text>
|
||||
<Progress value={90} size="lg" radius="xl" color="green" style={{ flex: 1 }} />
|
||||
</Group>
|
||||
<Group align="center" gap="md">
|
||||
<Text size="sm" fw={500} w={60}>
|
||||
Pembangunan
|
||||
</Text>
|
||||
<Progress value={50} size="lg" radius="xl" color="orange" style={{ flex: 1 }} />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
<ChartAPBDes />
|
||||
|
||||
{/* Section 6: SDGs Desa Cards */}
|
||||
<Grid gutter="md">
|
||||
{sdgsData.map((sdg, index) => (
|
||||
<Grid.Col key={index} span={{ base: 9, md: 3 }}>
|
||||
<SDGSCard
|
||||
image={<Image src={sdg.image} alt={sdg.title} />}
|
||||
title={sdg.title}
|
||||
score={sdg.score}
|
||||
/>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
70
src/components/dashboard/activity-list.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { Calendar } from "lucide-react";
|
||||
|
||||
interface EventData {
|
||||
date: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const events: EventData[] = [
|
||||
{ date: "1 Oktober 2025", title: "Hari Kesaktian Pancasila" },
|
||||
{ date: "15 Oktober 2025", title: "Davest" },
|
||||
{ date: "19 Oktober 2025", title: "Rapat Koordinasi" },
|
||||
];
|
||||
|
||||
export function ActivityList() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group gap="xs" mb="lg">
|
||||
<Calendar
|
||||
style={{ width: 20, height: 20 }}
|
||||
color={dark ? "#E2E8F0" : "#1E3A5F"}
|
||||
/>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"}>
|
||||
Kalender & Kegiatan Mendatang
|
||||
</Title>
|
||||
</Group>
|
||||
<Stack gap="md">
|
||||
{events.map((event, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
style={{
|
||||
borderLeft: "4px solid var(--mantine-color-blue-filled)",
|
||||
paddingLeft: 12,
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c="dimmed">
|
||||
{event.date}
|
||||
</Text>
|
||||
<Text fw={500} c={dark ? "white" : "gray.9"}>
|
||||
{event.title}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
75
src/components/dashboard/chart-apbdes.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
Cell,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
const apbdesData = [
|
||||
{ name: "Belanja", value: 70, color: "#3B82F6" },
|
||||
{ name: "Pangan", value: 45, color: "#22C55E" },
|
||||
{ name: "Pembiayaan", value: 55, color: "#FACC15" },
|
||||
{ name: "Pendapatan", value: 90, color: "#3B82F6" },
|
||||
];
|
||||
|
||||
export function ChartAPBDes() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||
Grafik APBDes
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
{apbdesData.map((item, index) => (
|
||||
<Group key={index} align="center" gap="md">
|
||||
<Text size="sm" fw={500} w={100} c={dark ? "white" : "gray.7"}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<ResponsiveContainer width="100%" height={20}>
|
||||
<BarChart layout="vertical" data={[item]}>
|
||||
<XAxis type="number" hide domain={[0, 100]} />
|
||||
<YAxis type="category" hide dataKey="name" />
|
||||
<Tooltip
|
||||
formatter={(value: number) => [`${value}%`, ""]}
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="value" radius={[4, 4, 4, 4]}>
|
||||
<Cell fill={item.color} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
111
src/components/dashboard/chart-surat.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
const chartData = [
|
||||
{ month: "Jan", value: 150 },
|
||||
{ month: "Feb", value: 165 },
|
||||
{ month: "Mar", value: 195 },
|
||||
{ month: "Apr", value: 160 },
|
||||
{ month: "Mei", value: 205 },
|
||||
{ month: "Jun", value: 185 },
|
||||
];
|
||||
|
||||
export function ChartSurat() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Box>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
||||
Statistik Pengajuan Surat
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
Trend pengajuan surat 6 bulan terakhir
|
||||
</Text>
|
||||
</Box>
|
||||
<ActionIcon variant="subtle" size="lg" radius="md">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 5L13 10L8 15"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
ticks={[0, 55, 110, 165, 220]}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="var(--mantine-color-blue-filled)"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
70
src/components/dashboard/division-progress.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
|
||||
interface DivisionData {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const divisionData: DivisionData[] = [
|
||||
{ name: "Kesejahteraan", value: 37 },
|
||||
{ name: "Pemberdayaan", value: 26 },
|
||||
{ name: "Keuangan", value: 17 },
|
||||
{ name: "Sekretaris Desa", value: 15 },
|
||||
];
|
||||
|
||||
const max_value = 37;
|
||||
|
||||
export function DivisionProgress() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||
Divisi Teraktif
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{divisionData.map((divisi, index) => (
|
||||
<Box key={index}>
|
||||
<Group justify="space-between" mb={5}>
|
||||
<Text size="sm" fw={500} c={dark ? "white" : "gray.7"}>
|
||||
{divisi.name}
|
||||
</Text>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||
{divisi.value} Kegiatan
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={(divisi.value / max_value) * 100}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
color="blue"
|
||||
animated
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
7
src/components/dashboard/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { ActivityList } from "./activity-list";
|
||||
export { ChartAPBDes } from "./chart-apbdes";
|
||||
export { ChartSurat } from "./chart-surat";
|
||||
export { DivisionProgress } from "./division-progress";
|
||||
export { SatisfactionChart } from "./satisfaction-chart";
|
||||
export { SDGSCard } from "./sdgs-card";
|
||||
export { StatCard } from "./stat-card";
|
||||
83
src/components/dashboard/satisfaction-chart.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
||||
|
||||
const satisfactionData = [
|
||||
{ name: "Sangat Puas", value: 25, color: "#4E5BA6" },
|
||||
{ name: "Puas", value: 25, color: "#F4C542" },
|
||||
{ name: "Cukup", value: 25, color: "#8CC63F" },
|
||||
{ name: "Kurang", value: 25, color: "#E57373" },
|
||||
];
|
||||
|
||||
export function SatisfactionChart() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
||||
Tingkat Kepuasan
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed" mb="md">
|
||||
Tingkat kepuasan layanan
|
||||
</Text>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={satisfactionData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={80}
|
||||
outerRadius={120}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{satisfactionData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<Group justify="center" gap="md" mt="md">
|
||||
{satisfactionData.map((item, index) => (
|
||||
<Group key={index} gap="xs">
|
||||
<Box
|
||||
w={12}
|
||||
h={12}
|
||||
style={{ backgroundColor: item.color, borderRadius: "50%" }}
|
||||
/>
|
||||
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
44
src/components/dashboard/sdgs-card.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Box, Card, Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface SDGSCardProps {
|
||||
title: string;
|
||||
score: number;
|
||||
image: ReactNode;
|
||||
}
|
||||
|
||||
export function SDGSCard({ title, score, image }: SDGSCardProps) {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box>{image}</Box>
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text
|
||||
ta={"center"}
|
||||
size="sm"
|
||||
c={dark ? "white" : "gray.8"}
|
||||
fw={500}
|
||||
mb="xs"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text ta={"center"} size="xl" c={dark ? "white" : "gray.8"} fw={700}>
|
||||
{score.toFixed(2)}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
86
src/components/dashboard/stat-card.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
detail?: string;
|
||||
trend?: string;
|
||||
trendValue?: number;
|
||||
icon: ReactNode;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
export function StatCard({
|
||||
title,
|
||||
value,
|
||||
detail,
|
||||
trend,
|
||||
trendValue,
|
||||
icon,
|
||||
iconColor = "darmasaba-blue",
|
||||
}: StatCardProps) {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
const isPositiveTrend = trendValue ? trendValue >= 0 : true;
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" w="100%">
|
||||
<Box style={{ flex: 1 }}>
|
||||
<Text size="sm" c="dimmed" mb="xs">
|
||||
{title}
|
||||
</Text>
|
||||
<Group align="baseline" gap="xs">
|
||||
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
|
||||
{value}
|
||||
</Text>
|
||||
</Group>
|
||||
{detail && (
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
{detail}
|
||||
</Text>
|
||||
)}
|
||||
{trend && (
|
||||
<Text
|
||||
size="sm"
|
||||
c={isPositiveTrend ? "green" : "red"}
|
||||
mt="xs"
|
||||
fw={500}
|
||||
>
|
||||
{trend}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<ThemeIcon
|
||||
variant="filled"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
color={dark ? "gray" : iconColor}
|
||||
>
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
import React from "react";
|
||||
import { BarChart, PieChart } from "@mantine/charts";
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Grid,
|
||||
Group,
|
||||
Stack,
|
||||
Grid,
|
||||
Box,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconBabyCarriage, IconSkull, IconArrowUp, IconArrowDown } from "@tabler/icons-react";
|
||||
import { BarChart, PieChart } from "@mantine/charts";
|
||||
import {
|
||||
IconArrowDown,
|
||||
IconArrowUp,
|
||||
IconBabyCarriage,
|
||||
IconSkull,
|
||||
} from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
|
||||
// Sample Data
|
||||
const kpiData = [
|
||||
@@ -71,7 +76,11 @@ const kpiData = [
|
||||
value: "23",
|
||||
sub: "Tahun ini",
|
||||
icon: (
|
||||
<IconBabyCarriage className="h-6 w-6 text-muted-foreground" role="img" aria-label="Icon kelahiran" />
|
||||
<IconBabyCarriage
|
||||
className="h-6 w-6 text-muted-foreground"
|
||||
role="img"
|
||||
aria-label="Icon kelahiran"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -136,10 +145,30 @@ const banjarData = [
|
||||
];
|
||||
|
||||
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" },
|
||||
{
|
||||
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 = () => {
|
||||
@@ -152,14 +181,22 @@ const DemografiPekerjaan = () => {
|
||||
<Grid gutter="lg">
|
||||
{kpiData.map((kpi) => (
|
||||
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" mb="xs">
|
||||
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
|
||||
{kpi.title}
|
||||
</Text>
|
||||
{React.cloneElement(kpi.icon, {
|
||||
className: "h-6 w-6",
|
||||
color: dark ? "var(--mantine-color-dark-3)" : "var(--mantine-color-dimmed)",
|
||||
color: dark
|
||||
? "var(--mantine-color-dark-3)"
|
||||
: "var(--mantine-color-dimmed)",
|
||||
})}
|
||||
</Group>
|
||||
<Title order={3} fw={700} c={dark ? "dark.0" : "black"} mt="xs">
|
||||
@@ -173,7 +210,9 @@ const DemografiPekerjaan = () => {
|
||||
? "green"
|
||||
: kpi.deltaType === "negative"
|
||||
? "red"
|
||||
: dark ? "dark.3" : "dimmed"
|
||||
: dark
|
||||
? "dark.3"
|
||||
: "dimmed"
|
||||
}
|
||||
mt={4}
|
||||
>
|
||||
@@ -194,7 +233,13 @@ const DemografiPekerjaan = () => {
|
||||
<Grid gutter="lg">
|
||||
{/* Grafik Pengelompokan Umur */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Grafik Pengelompokan Umur
|
||||
</Title>
|
||||
@@ -202,7 +247,7 @@ const DemografiPekerjaan = () => {
|
||||
h={300}
|
||||
data={ageDistributionData}
|
||||
dataKey="ageRange"
|
||||
series={[{ name: 'total', color: 'darmasaba-navy' }]}
|
||||
series={[{ name: "total", color: "darmasaba-navy" }]}
|
||||
withLegend
|
||||
/>
|
||||
</Card>
|
||||
@@ -210,7 +255,13 @@ const DemografiPekerjaan = () => {
|
||||
|
||||
{/* Demografi Pekerjaan */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Demografi Pekerjaan
|
||||
</Title>
|
||||
@@ -218,7 +269,7 @@ const DemografiPekerjaan = () => {
|
||||
h={300}
|
||||
data={jobDistributionData}
|
||||
dataKey="job"
|
||||
series={[{ name: 'total', color: 'darmasaba-navy' }]}
|
||||
series={[{ name: "total", color: "darmasaba-navy" }]}
|
||||
withLegend
|
||||
/>
|
||||
</Card>
|
||||
@@ -229,16 +280,22 @@ const DemografiPekerjaan = () => {
|
||||
<Grid gutter="lg">
|
||||
{/* Distribusi Agama */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Distribusi Agama
|
||||
</Title>
|
||||
<PieChart
|
||||
h={300}
|
||||
data={religionData.map(item => ({
|
||||
data={religionData.map((item) => ({
|
||||
name: item.religion,
|
||||
value: item.total,
|
||||
color: item.color
|
||||
color: item.color,
|
||||
}))}
|
||||
withLabels
|
||||
withLabelsLine
|
||||
@@ -250,27 +307,53 @@ const DemografiPekerjaan = () => {
|
||||
|
||||
{/* 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" }}>
|
||||
<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.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>
|
||||
<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>
|
||||
))}
|
||||
@@ -281,14 +364,29 @@ const DemografiPekerjaan = () => {
|
||||
</Grid>
|
||||
|
||||
{/* Statistik Dinamika Penduduk */}
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<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">
|
||||
Statistik Dinamika Penduduk
|
||||
</Title>
|
||||
<Grid gutter="md">
|
||||
{dynamicStats.map((stat, index) => (
|
||||
<Grid.Col key={`${stat.title}-${index}`} span={{ base: 12, md: 3 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Grid.Col
|
||||
key={`${stat.title}-${index}`}
|
||||
span={{ base: 12, md: 3 }}
|
||||
>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
<Box>
|
||||
<Text size="sm" fw={500} c={dark ? "dark.3" : "dimmed"}>
|
||||
@@ -298,9 +396,7 @@ const DemografiPekerjaan = () => {
|
||||
{stat.value}
|
||||
</Title>
|
||||
</Box>
|
||||
<Box c={stat.color}>
|
||||
{stat.icon}
|
||||
</Box>
|
||||
<Box c={stat.color}>{stat.icon}</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
@@ -312,4 +408,4 @@ const DemografiPekerjaan = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DemografiPekerjaan;
|
||||
export default DemografiPekerjaan;
|
||||
|
||||
@@ -1,46 +1,52 @@
|
||||
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 {
|
||||
ActionIcon,
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Divider,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
ActionIcon,
|
||||
Divider,
|
||||
Avatar,
|
||||
Box,
|
||||
Badge,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconUserShield } from "@tabler/icons-react";
|
||||
import { useLocation, useNavigate } 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
|
||||
|
||||
export function Header() {
|
||||
const location = useLocation();
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Define page titles based on route
|
||||
const getPageTitle = () => {
|
||||
switch (location.pathname) {
|
||||
case "/":
|
||||
return "Desa Darmasaba";
|
||||
return "Beranda";
|
||||
case "/kinerja-divisi":
|
||||
return "Kinerja Divisi";
|
||||
case "/pengaduan":
|
||||
case "/pengaduan-layanan-publik":
|
||||
return "Pengaduan & Layanan Publik";
|
||||
case "/analytic":
|
||||
case "/jenna-analytic":
|
||||
return "Jenna Analytic";
|
||||
case "/demografi":
|
||||
case "/demografi-pekerjaan":
|
||||
return "Demografi & Kependudukan";
|
||||
case "/keuangan":
|
||||
case "/keuangan-anggaran":
|
||||
return "Keuangan & Anggaran";
|
||||
case "/bumdes":
|
||||
return "Bumdes & UMKM Desa";
|
||||
case "/sosial":
|
||||
return "Sosial";
|
||||
case "/keamanan":
|
||||
return "Keamanan";
|
||||
case "/bantuan":
|
||||
return "Bantuan";
|
||||
case "/pengaturan":
|
||||
case "/pengaturan/umum":
|
||||
case "/pengaturan/notifikasi":
|
||||
case "/pengaturan/keamanan":
|
||||
case "/pengaturan/akses-dan-tim":
|
||||
return "Pengaturan";
|
||||
default:
|
||||
return "Desa Darmasaba";
|
||||
@@ -50,12 +56,12 @@ export function Header() {
|
||||
return (
|
||||
<Group justify="space-between" w="100%">
|
||||
{/* Title */}
|
||||
<Title order={3} c={"white"}>{getPageTitle()}</Title>
|
||||
<Title order={3} c={"white"}>
|
||||
{getPageTitle()}
|
||||
</Title>
|
||||
|
||||
{/* Right Section */}
|
||||
<Group gap="md">
|
||||
|
||||
|
||||
{/* User Info */}
|
||||
<Group gap="sm">
|
||||
<Box ta="right">
|
||||
@@ -101,6 +107,13 @@ export function Header() {
|
||||
10
|
||||
</Badge>
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="subtle" size="lg" radius="xl">
|
||||
<IconUserShield
|
||||
color="white"
|
||||
style={{ width: "70%", height: "70%" }}
|
||||
onClick={() => navigate({ to: "/signin" })}
|
||||
/>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -1,248 +1,435 @@
|
||||
import { Container, Grid, Title, Text, SimpleGrid, Box, Accordion, Stack, useMantineColorScheme } from '@mantine/core';
|
||||
import { HelpCard } from '@/components/ui/help-card';
|
||||
import { IconBook, IconVideo, IconHelpCircle, IconMessage, IconFileText, IconHeadphones } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Accordion,
|
||||
Box,
|
||||
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 { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
// Sample data for sections
|
||||
const guideItems = [
|
||||
{ title: 'Cara Login', description: 'Langkah-langkah untuk login ke dashboard' },
|
||||
{ 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 { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
// Sample data for sections
|
||||
const guideItems = [
|
||||
{
|
||||
title: "Cara Login",
|
||||
description: "Langkah-langkah untuk login ke dashboard",
|
||||
},
|
||||
{
|
||||
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 = [
|
||||
{ title: 'Dashboard Overview', duration: '5:23' },
|
||||
{ title: 'Analisis Data', duration: '8:45' },
|
||||
{ title: 'Membuat Laporan', duration: '6:12' },
|
||||
{ title: 'Export Data', duration: '4:30' },
|
||||
];
|
||||
const videoItems = [
|
||||
{ title: "Dashboard Overview", duration: "5:23" },
|
||||
{ title: "Analisis Data", duration: "8:45" },
|
||||
{ title: "Membuat Laporan", duration: "6:12" },
|
||||
{ title: "Export Data", duration: "4:30" },
|
||||
];
|
||||
|
||||
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: '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 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: "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 = [
|
||||
{ title: 'API Reference', description: 'Dokumentasi lengkap untuk integrasi API' },
|
||||
{ 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 documentationItems = [
|
||||
{
|
||||
title: "API Reference",
|
||||
description: "Dokumentasi lengkap untuk integrasi API",
|
||||
},
|
||||
{
|
||||
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 = [
|
||||
{ value: '150+', label: 'Artikel Panduan' },
|
||||
{ value: '50+', label: 'Video Tutorial' },
|
||||
{ value: '24/7', label: 'Support Aktif' },
|
||||
];
|
||||
const stats = [
|
||||
{ value: "150+", label: "Artikel Panduan" },
|
||||
{ value: "50+", label: "Video Tutorial" },
|
||||
{ value: "24/7", label: "Support Aktif" },
|
||||
];
|
||||
|
||||
// State for chat functionality
|
||||
const [messages, setMessages] = useState([
|
||||
{ id: 1, text: 'Halo! Saya Jenna, asisten virtual Anda. Bagaimana saya bisa membantu hari ini?', sender: 'jenna' }
|
||||
]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// State for chat functionality
|
||||
const [messages, setMessages] = useState([
|
||||
{
|
||||
id: 1,
|
||||
text: "Halo! Saya Jenna, asisten virtual Anda. Bagaimana saya bisa membantu hari ini?",
|
||||
sender: "jenna",
|
||||
},
|
||||
]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (inputValue.trim() === '') return;
|
||||
const handleSendMessage = () => {
|
||||
if (inputValue.trim() === "") return;
|
||||
|
||||
// Add user message
|
||||
const newUserMessage = {
|
||||
id: messages.length + 1,
|
||||
text: inputValue,
|
||||
sender: 'user'
|
||||
};
|
||||
// Add user message
|
||||
const newUserMessage = {
|
||||
id: messages.length + 1,
|
||||
text: inputValue,
|
||||
sender: "user",
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, newUserMessage]);
|
||||
setInputValue('');
|
||||
setIsLoading(true);
|
||||
setMessages((prev) => [...prev, newUserMessage]);
|
||||
setInputValue("");
|
||||
setIsLoading(true);
|
||||
|
||||
// Simulate Jenna's response after delay
|
||||
setTimeout(() => {
|
||||
const jennaResponse = {
|
||||
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.',
|
||||
sender: 'jenna'
|
||||
};
|
||||
setMessages(prev => [...prev, jennaResponse]);
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
// Simulate Jenna's response after delay
|
||||
setTimeout(() => {
|
||||
const jennaResponse = {
|
||||
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.",
|
||||
sender: "jenna",
|
||||
};
|
||||
setMessages((prev) => [...prev, jennaResponse]);
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={1} mb="xl" ta="center">Pusat Bantuan</Title>
|
||||
<Text size="lg" color="dimmed" ta="center" mb="xl">
|
||||
Temukan jawaban untuk pertanyaan Anda atau hubungi tim support kami
|
||||
</Text>
|
||||
return (
|
||||
<Container size="lg" py="xl">
|
||||
<Title order={1} mb="xl" ta="center">
|
||||
Pusat Bantuan
|
||||
</Title>
|
||||
<Text size="lg" color="dimmed" ta="center" mb="xl">
|
||||
Temukan jawaban untuk pertanyaan Anda atau hubungi tim support kami
|
||||
</Text>
|
||||
|
||||
{/* Statistics Section */}
|
||||
<SimpleGrid cols={3} spacing="lg" mb="xl">
|
||||
{stats.map((stat, index) => (
|
||||
<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 */}
|
||||
<SimpleGrid cols={3} spacing="lg" mb="xl">
|
||||
{stats.map((stat, index) => (
|
||||
<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>
|
||||
|
||||
<Stack gap="lg">
|
||||
<Box>
|
||||
<Grid gutter="lg" justify="center">
|
||||
{/* Panduan Memulai */}
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} 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">
|
||||
<Box>
|
||||
<Grid gutter="lg" justify="center">
|
||||
{/* Panduan Memulai */}
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<HelpCard
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
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>
|
||||
|
||||
{/* Video Tutorial */}
|
||||
<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%">
|
||||
<Box>
|
||||
{videoItems.map((item, index) => (
|
||||
<Box key={index} py="sm" style={{ 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 */}
|
||||
<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%"
|
||||
>
|
||||
<Box>
|
||||
{videoItems.map((item, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
py="sm"
|
||||
style={{
|
||||
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>
|
||||
|
||||
{/* FAQ */}
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconHelpCircle size={24} />} title="FAQ" h="100%">
|
||||
<Accordion variant="separated" >
|
||||
{faqItems.map((item, index) => (
|
||||
<Accordion.Item 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 */}
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<HelpCard
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
icon={<IconHelpCircle size={24} />}
|
||||
title="FAQ"
|
||||
h="100%"
|
||||
>
|
||||
<Accordion variant="separated">
|
||||
{faqItems.map((item, index) => (
|
||||
<Accordion.Item
|
||||
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>
|
||||
|
||||
<Box>
|
||||
<Grid>
|
||||
{/* Hubungi Support */}
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<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>
|
||||
<Box>
|
||||
<Grid>
|
||||
{/* Hubungi Support */}
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<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 size="sm" color="dimmed" mb="md"><a href="https://wa.me/1234567890">+62 123 456 7890</a></Text>
|
||||
<Text fw={500}>WhatsApp</Text>
|
||||
<Text size="sm" color="dimmed" mb="md">
|
||||
<a href="https://wa.me/1234567890">+62 123 456 7890</a>
|
||||
</Text>
|
||||
|
||||
<Text fw={500}>Jam Kerja</Text>
|
||||
<Text size="sm" color="dimmed">Senin - Jumat, 09:00 - 17:00 WIB</Text>
|
||||
<Text fw={500}>Jam Kerja</Text>
|
||||
<Text size="sm" color="dimmed">
|
||||
Senin - Jumat, 09:00 - 17:00 WIB
|
||||
</Text>
|
||||
|
||||
<Text fw={500} mt="md">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>
|
||||
<Text size="sm" color="dimmed">
|
||||
Rata-rata 2-4 jam kerja
|
||||
</Text>
|
||||
</Box>
|
||||
</HelpCard>
|
||||
</Grid.Col>
|
||||
|
||||
{/* Dokumentasi */}
|
||||
<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%">
|
||||
<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 */}
|
||||
<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%"
|
||||
>
|
||||
<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>
|
||||
|
||||
{/* Jenna - Virtual Assistant */}
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<HelpCard style={{ borderColor: dark ? "#141D34" : "white" }} bg={dark ? "#141D34" : "white"} icon={<IconMessage size={24} />} title="Jenna - Virtual Assistant" h="100%">
|
||||
<Box style={{ height: '300px', 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 */}
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<HelpCard
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
icon={<IconMessage size={24} />}
|
||||
title="Jenna - Virtual Assistant"
|
||||
h="100%"
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
height: "300px",
|
||||
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>
|
||||
|
||||
<Box style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Ketik pesan Anda..."
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
borderRadius: '20px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
<Box style={{ display: "flex", gap: "8px" }}>
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Ketik pesan Anda..."
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "8px 12px",
|
||||
borderRadius: "20px",
|
||||
border: "1px solid #ccc",
|
||||
}}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<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;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from "react";
|
||||
import { BarChart } from "@mantine/charts";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Badge,
|
||||
Progress,
|
||||
Title,
|
||||
Text,
|
||||
Group,
|
||||
Stack,
|
||||
Grid,
|
||||
Box,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { BarChart } from "@mantine/charts";
|
||||
import React from "react";
|
||||
|
||||
// Sample Data
|
||||
const kpiData = [
|
||||
@@ -144,7 +144,13 @@ const JennaAnalytic = () => {
|
||||
<Grid gutter="lg">
|
||||
{kpiData.map((kpi) => (
|
||||
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" mb="xs">
|
||||
<Text size="sm" fw={500} c="dimmed">
|
||||
{kpi.title}
|
||||
@@ -182,7 +188,13 @@ const JennaAnalytic = () => {
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Interaksi Chatbot
|
||||
</Title>
|
||||
@@ -190,7 +202,7 @@ const JennaAnalytic = () => {
|
||||
h={300}
|
||||
data={chartData}
|
||||
dataKey="day"
|
||||
series={[{ name: 'total', color: 'blue' }]}
|
||||
series={[{ name: "total", color: "blue" }]}
|
||||
withLegend
|
||||
/>
|
||||
</Card>
|
||||
@@ -199,16 +211,21 @@ const JennaAnalytic = () => {
|
||||
<Grid gutter="lg">
|
||||
{/* Grafik Interaksi Chatbot (now Bar Chart) */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Jam Tersibuk
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{busyHours.map((item, index) => (
|
||||
<Box key={index}>
|
||||
<Text size="sm">
|
||||
{item.period}
|
||||
</Text>
|
||||
<Text size="sm">{item.period}</Text>
|
||||
<Group align="center">
|
||||
<Progress value={item.percentage} flex={1} />
|
||||
<Text size="sm" fw={500}>
|
||||
@@ -225,7 +242,14 @@ const JennaAnalytic = () => {
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Stack gap="lg">
|
||||
{/* Topik Pertanyaan Terbanyak */}
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Topik Pertanyaan Terbanyak
|
||||
</Title>
|
||||
@@ -251,12 +275,9 @@ const JennaAnalytic = () => {
|
||||
{/* Jam Tersibuk */}
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid >
|
||||
|
||||
</Stack >
|
||||
</Box >
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
|
||||
}
|
||||
};
|
||||
export default JennaAnalytic;
|
||||
|
||||
|
||||
@@ -1,225 +1,325 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
Stack,
|
||||
useMantineColorScheme,
|
||||
Badge,
|
||||
List,
|
||||
ThemeIcon,
|
||||
Box
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
List,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconCamera,
|
||||
IconAlertTriangle,
|
||||
IconMapPin,
|
||||
IconClock,
|
||||
IconEye,
|
||||
IconShieldLock
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconCamera,
|
||||
IconClock,
|
||||
IconEye,
|
||||
IconMapPin,
|
||||
IconShieldLock,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const KeamananPage = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
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",
|
||||
},
|
||||
];
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
// Sample data for CCTV locations
|
||||
const cctvLocations = [
|
||||
{ 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" },
|
||||
{ id: "CCTV-03", lat: -8.4, lng: 115.1, status: "offline", lastSeen: "1 hari yang lalu", location: "Taman Desa" },
|
||||
{ id: "CCTV-04", lat: -8.7, lng: 115.4, status: "active", lastSeen: "30 menit yang lalu", location: "Pasar Desa" },
|
||||
];
|
||||
// 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 security reports
|
||||
const securityReports = [
|
||||
{
|
||||
id: "REP-001",
|
||||
title: "Pencurian Motor",
|
||||
reportedAt: "2 jam yang lalu",
|
||||
date: "12 Feb 2026, 14:30",
|
||||
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",
|
||||
},
|
||||
];
|
||||
// Sample data for CCTV locations
|
||||
const cctvLocations = [
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
id: "CCTV-03",
|
||||
lat: -8.4,
|
||||
lng: 115.1,
|
||||
status: "offline",
|
||||
lastSeen: "1 hari yang lalu",
|
||||
location: "Taman Desa",
|
||||
},
|
||||
{
|
||||
id: "CCTV-04",
|
||||
lat: -8.7,
|
||||
lng: 115.4,
|
||||
status: "active",
|
||||
lastSeen: "30 menit yang lalu",
|
||||
location: "Pasar Desa",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Page Header */}
|
||||
<Group justify="space-between" align="center">
|
||||
<Title order={2} c={dark ? "dark.0" : "black"}>
|
||||
Keamanan Lingkungan Desa
|
||||
</Title>
|
||||
</Group>
|
||||
// Sample data for security reports
|
||||
const securityReports = [
|
||||
{
|
||||
id: "REP-001",
|
||||
title: "Pencurian Motor",
|
||||
reportedAt: "2 jam yang lalu",
|
||||
date: "12 Feb 2026, 14:30",
|
||||
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 */}
|
||||
<Grid gutter="md">
|
||||
{kpiData.map((kpi, index) => (
|
||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Group justify="space-between" align="center">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{kpi.subtitle}
|
||||
</Text>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{kpi.value}
|
||||
</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{kpi.title}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
<ThemeIcon variant="light" color={kpi.color} size="xl" radius="xl">
|
||||
{kpi.icon}
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Page Header */}
|
||||
<Group justify="space-between" align="center">
|
||||
<Title order={2} c={dark ? "dark.0" : "black"}>
|
||||
Keamanan Lingkungan Desa
|
||||
</Title>
|
||||
</Group>
|
||||
|
||||
<Grid gutter="md">
|
||||
{/* Peta Keamanan CCTV */}
|
||||
<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"}>Peta Keamanan CCTV</Title>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">Titik Lokasi CCTV</Text>
|
||||
|
||||
{/* Placeholder for map */}
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: dark ? '#2d3748' : '#e2e8f0',
|
||||
borderRadius: '8px',
|
||||
height: '400px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
{/* KPI Cards */}
|
||||
<Grid gutter="md">
|
||||
{kpiData.map((kpi, index) => (
|
||||
<GridCol key={index} span={{ base: 12, sm: 6, md: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{kpi.subtitle}
|
||||
</Text>
|
||||
<Group gap="xs" align="center">
|
||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{kpi.value}
|
||||
</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{kpi.title}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
color={kpi.color}
|
||||
size="xl"
|
||||
radius="xl"
|
||||
>
|
||||
{kpi.icon}
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
<Grid gutter="md">
|
||||
{/* Peta Keamanan CCTV */}
|
||||
<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"}>
|
||||
Peta Keamanan CCTV
|
||||
</Title>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"} mb="md">
|
||||
Titik Lokasi CCTV
|
||||
</Text>
|
||||
|
||||
{/* Placeholder for map */}
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: dark ? "#2d3748" : "#e2e8f0",
|
||||
borderRadius: "8px",
|
||||
height: "400px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<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 */}
|
||||
<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;
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import React from "react";
|
||||
import { BarChart } from "@mantine/charts";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Badge,
|
||||
Title,
|
||||
Text,
|
||||
Group,
|
||||
Stack,
|
||||
Grid,
|
||||
Box,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconTrendingUp, IconTrendingDown, IconCurrency } from "@tabler/icons-react";
|
||||
import { BarChart } from "@mantine/charts";
|
||||
import {
|
||||
IconCurrency,
|
||||
IconTrendingDown,
|
||||
IconTrendingUp,
|
||||
} from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
|
||||
// Sample Data
|
||||
const kpiData = [
|
||||
@@ -22,9 +26,7 @@ const kpiData = [
|
||||
title: "Total APBDes",
|
||||
value: "Rp 5.2M",
|
||||
sub: "Tahun 2025",
|
||||
icon: (
|
||||
<IconCurrency className="h-6 w-6 text-muted-foreground" />
|
||||
),
|
||||
icon: <IconCurrency className="h-6 w-6 text-muted-foreground" />,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -55,18 +57,14 @@ const kpiData = [
|
||||
sub: "Bulan ini",
|
||||
delta: "+8%",
|
||||
deltaType: "positive",
|
||||
icon: (
|
||||
<IconTrendingUp className="h-6 w-6 text-muted-foreground" />
|
||||
),
|
||||
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" />
|
||||
),
|
||||
icon: <IconTrendingDown className="h-6 w-6 text-muted-foreground" />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -125,7 +123,14 @@ const KeuanganAnggaran = () => {
|
||||
<Grid gutter="lg">
|
||||
{kpiData.map((kpi) => (
|
||||
<Grid.Col key={kpi.id} span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="flex-start" mb="xs">
|
||||
<Text size="sm" fw={500} c="dimmed">
|
||||
{kpi.title}
|
||||
@@ -167,7 +172,13 @@ const KeuanganAnggaran = () => {
|
||||
<Grid gutter="lg">
|
||||
{/* Grafik Pemasukan vs Pengeluaran */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Pemasukan vs Pengeluaran
|
||||
</Title>
|
||||
@@ -176,8 +187,8 @@ const KeuanganAnggaran = () => {
|
||||
data={incomeExpenseData}
|
||||
dataKey="month"
|
||||
series={[
|
||||
{ name: 'income', color: 'green', label: 'Pemasukan' },
|
||||
{ name: 'expense', color: 'red', label: 'Pengeluaran' },
|
||||
{ name: "income", color: "green", label: "Pemasukan" },
|
||||
{ name: "expense", color: "red", label: "Pengeluaran" },
|
||||
]}
|
||||
withLegend
|
||||
/>
|
||||
@@ -186,7 +197,13 @@ const KeuanganAnggaran = () => {
|
||||
|
||||
{/* Alokasi Anggaran Per Sektor */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<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>
|
||||
@@ -194,7 +211,9 @@ const KeuanganAnggaran = () => {
|
||||
h={300}
|
||||
data={allocationData}
|
||||
dataKey="sector"
|
||||
series={[{ name: 'amount', color: 'darmasaba-navy', label: 'Jumlah' }]}
|
||||
series={[
|
||||
{ name: "amount", color: "darmasaba-navy", label: "Jumlah" },
|
||||
]}
|
||||
withLegend
|
||||
orientation="horizontal"
|
||||
/>
|
||||
@@ -205,7 +224,13 @@ const KeuanganAnggaran = () => {
|
||||
<Grid gutter="lg">
|
||||
{/* Dana Bantuan & Hibah */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Dana Bantuan & Hibah
|
||||
</Title>
|
||||
@@ -243,13 +268,21 @@ const KeuanganAnggaran = () => {
|
||||
|
||||
{/* Laporan APBDes */}
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} fw={500} mb="md">
|
||||
Laporan APBDes
|
||||
</Title>
|
||||
|
||||
<Box mb="md">
|
||||
<Title order={4} mb="sm">Pendapatan</Title>
|
||||
<Title order={4} mb="sm">
|
||||
Pendapatan
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
{apbdReport.income.map((item, index) => (
|
||||
<Group key={index} justify="space-between">
|
||||
@@ -269,7 +302,9 @@ const KeuanganAnggaran = () => {
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Title order={4} mb="sm">Belanja</Title>
|
||||
<Title order={4} mb="sm">
|
||||
Belanja
|
||||
</Title>
|
||||
<Stack gap="xs">
|
||||
{apbdReport.expenses.map((item, index) => (
|
||||
<Group key={index} justify="space-between">
|
||||
@@ -288,11 +323,26 @@ const KeuanganAnggaran = () => {
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Box mt="md" pt="md" style={{ borderTop: '1px solid var(--mantine-color-gray-3)' }}>
|
||||
<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
|
||||
fw={700}
|
||||
c={
|
||||
apbdReport.totalIncome > apbdReport.totalExpenses
|
||||
? "green"
|
||||
: "red"
|
||||
}
|
||||
>
|
||||
Rp{" "}
|
||||
{(
|
||||
apbdReport.totalIncome - apbdReport.totalExpenses
|
||||
).toLocaleString()}
|
||||
jt
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
@@ -304,4 +354,4 @@ const KeuanganAnggaran = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default KeuanganAnggaran;
|
||||
export default KeuanganAnggaran;
|
||||
|
||||
@@ -1,343 +1,100 @@
|
||||
import {
|
||||
Stack,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
ActionIcon,
|
||||
Progress as MantineProgress,
|
||||
Box,
|
||||
Badge as MantineBadge,
|
||||
Card,
|
||||
useMantineColorScheme,
|
||||
ThemeIcon,
|
||||
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";
|
||||
import { Grid, Stack } from "@mantine/core";
|
||||
import { ActivityCard } from "./kinerja-divisi/activity-card";
|
||||
import { DivisionList } from "./kinerja-divisi/division-list";
|
||||
import { DocumentChart } from "./kinerja-divisi/document-chart";
|
||||
import { ProgressChart } from "./kinerja-divisi/progress-chart";
|
||||
import { DiscussionPanel } from "./kinerja-divisi/discussion-panel";
|
||||
import { EventCard } from "./kinerja-divisi/event-card";
|
||||
import { ArchiveCard } from "./kinerja-divisi/archive-card";
|
||||
|
||||
|
||||
// Data for program kegiatan (Section 1)
|
||||
const programKegiatanData = [
|
||||
{
|
||||
title: "Rakor 2025",
|
||||
date: "3 Juli 2025",
|
||||
progress: 90,
|
||||
status: "Selesai" as const,
|
||||
},
|
||||
{
|
||||
title: "Pemutakhiran Indeks Desa",
|
||||
date: "3 Juli 2025",
|
||||
progress: 85,
|
||||
status: "Selesai" as const,
|
||||
},
|
||||
{
|
||||
title: "Mengurus Akta Cerai Warga",
|
||||
date: "3 Juli 2025",
|
||||
progress: 80,
|
||||
status: "Selesai" as const,
|
||||
},
|
||||
{
|
||||
title: "Pasek 7 Desa Adat",
|
||||
date: "3 Juli 2025",
|
||||
progress: 92,
|
||||
status: "Selesai" as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Data for arsip digital (Section 5)
|
||||
const archiveData = [
|
||||
{ name: "Surat Keputusan" },
|
||||
{ name: "Dokumentasi" },
|
||||
{ name: "Laporan Keuangan" },
|
||||
{ name: "Notulensi Rapat" },
|
||||
];
|
||||
|
||||
const KinerjaDivisi = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
|
||||
// Data for division progress chart
|
||||
const divisionProgressData = [
|
||||
{ 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",
|
||||
tasks: [
|
||||
{ title: "Laporan Bulanan", status: "selesai" },
|
||||
{ title: "Arsip Dokumen", status: "berjalan" },
|
||||
{ title: "Undangan Rapat", status: "tertunda" },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Keuangan",
|
||||
tasks: [
|
||||
{ title: "Laporan APBDes", status: "selesai" },
|
||||
{ title: "Verifikasi Dana", status: "tertunda" },
|
||||
{ title: "Pengeluaran Harian", status: "berjalan" },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Sosial",
|
||||
tasks: [
|
||||
{ title: "Program Bantuan", status: "selesai" },
|
||||
{ title: "Kegiatan Posyandu", status: "berjalan" },
|
||||
{ title: "Monitoring Stunting", status: "tertunda" },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Humas",
|
||||
tasks: [
|
||||
{ title: "Publikasi Kegiatan", status: "selesai" },
|
||||
{ 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
|
||||
const documentStats = [
|
||||
{ name: "Gambar", value: 42 },
|
||||
{ name: "Dokumen", value: 87 },
|
||||
];
|
||||
|
||||
// Activity progress statistics
|
||||
const activityProgressStats = [
|
||||
{ name: "Selesai", value: 12 },
|
||||
{ name: "Dikerjakan", value: 8 },
|
||||
{ name: "Segera Dikerjakan", value: 5 },
|
||||
{ name: "Dibatalkan", value: 2 },
|
||||
];
|
||||
|
||||
const COLORS = ['#10B981', '#F59E0B', '#EF4444', '#6B7280'];
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
selesai: 'green',
|
||||
berjalan: 'blue',
|
||||
tertunda: 'red',
|
||||
proses: 'yellow'
|
||||
};
|
||||
|
||||
// Discussion data
|
||||
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: "Festival Budaya", sender: "Divisi Humas", timestamp: "1 hari yang lalu" },
|
||||
];
|
||||
|
||||
// Today's agenda
|
||||
const todayAgenda = [
|
||||
{ time: "09:00", event: "Rapat Evaluasi Bulanan" },
|
||||
{ time: "14:00", event: "Koordinasi Program Bantuan" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Grafik Progres Tugas per Divisi */}
|
||||
<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'}>
|
||||
Grafik Progres Tugas per Divisi
|
||||
</Title>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={divisionProgressData}>
|
||||
<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="selesai" stackId="a" fill="#10B981" name="Selesai" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="berjalan" stackId="a" fill="#3B82F6" name="Berjalan" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="tertunda" stackId="a" fill="#EF4444" name="Tertunda" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
|
||||
{/* Ringkasan Tugas per Divisi */}
|
||||
{/* SECTION 1 — PROGRAM KEGIATAN */}
|
||||
<Grid gutter="md">
|
||||
{divisionTasks.map((division, index) => (
|
||||
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<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'}>
|
||||
{division.name}
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{division.tasks.map((task, taskIndex) => (
|
||||
<Box key={taskIndex}>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c={dark ? 'white' : 'darmasaba-navy'}>{task.title}</Text>
|
||||
<MantineBadge
|
||||
color={STATUS_COLORS[task.status] || 'gray'}
|
||||
variant="light"
|
||||
>
|
||||
{task.status}
|
||||
</MantineBadge>
|
||||
</Group>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
</GridCol>
|
||||
{programKegiatanData.map((kegiatan, index) => (
|
||||
<Grid.Col key={index} span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<ActivityCard
|
||||
title={kegiatan.title}
|
||||
date={kegiatan.date}
|
||||
progress={kegiatan.progress}
|
||||
status={kegiatan.status}
|
||||
/>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Arsip Digital Perangkat Desa */}
|
||||
<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'}>
|
||||
Arsip Digital Perangkat Desa
|
||||
</Title>
|
||||
<Grid gutter="md">
|
||||
{archiveItems.map((item, index) => (
|
||||
<GridCol key={index} span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<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>
|
||||
<Text c={dark ? 'white' : 'darmasaba-navy'} fw={700}>{item.count}</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
))}
|
||||
</Grid>
|
||||
</Card>
|
||||
{/* SECTION 2 — GRID DASHBOARD (3 Columns) */}
|
||||
<Grid gutter="lg">
|
||||
{/* Left Column - Division List */}
|
||||
<Grid.Col span={{ base: 12, lg: 3 }}>
|
||||
<DivisionList />
|
||||
</Grid.Col>
|
||||
|
||||
{/* Kartu Progres Kegiatan */}
|
||||
<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 / Program
|
||||
</Title>
|
||||
<Stack gap="md">
|
||||
{activityProgress.map((activity, 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 c={dark ? 'white' : 'darmasaba-navy'} fw={500}>{activity.name}</Text>
|
||||
<MantineBadge
|
||||
color={STATUS_COLORS[activity.status] || 'gray'}
|
||||
variant="light"
|
||||
>
|
||||
{activity.status}
|
||||
</MantineBadge>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<MantineProgress
|
||||
value={activity.progress}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
color={activity.progress === 100 ? "green" : "blue"}
|
||||
w="calc(100% - 80px)"
|
||||
/>
|
||||
<Text size="sm" c={dark ? 'white' : 'darmasaba-navy'}>{activity.progress}%</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed" mt="sm">{activity.date}</Text>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
{/* Middle Column - Document Chart */}
|
||||
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||
<DocumentChart />
|
||||
</Grid.Col>
|
||||
|
||||
{/* 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>
|
||||
<Tooltip
|
||||
contentStyle={dark
|
||||
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' }
|
||||
: {}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
</GridCol>
|
||||
{/* Right Column - Progress Chart */}
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<ProgressChart />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Diskusi Internal */}
|
||||
<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'}>
|
||||
Diskusi Internal
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{discussions.map((discussion, index) => (
|
||||
<Card key={index} 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}>{discussion.title}</Text>
|
||||
<Text size="sm" c="dimmed">{discussion.timestamp}</Text>
|
||||
</Group>
|
||||
<Text size="sm" c="dimmed">{discussion.sender}</Text>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
{/* SECTION 3 — DISCUSSION PANEL */}
|
||||
<DiscussionPanel />
|
||||
|
||||
{/* Agenda / Acara Hari Ini */}
|
||||
<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'}>
|
||||
Agenda / Acara Hari Ini
|
||||
</Title>
|
||||
{todayAgenda.length > 0 ? (
|
||||
<Stack gap="sm">
|
||||
{todayAgenda.map((agenda, index) => (
|
||||
<Group key={index} align="flex-start">
|
||||
<Box w={60}>
|
||||
<Text c="dimmed">{agenda.time}</Text>
|
||||
</Box>
|
||||
<Divider orientation="vertical" mx="sm" />
|
||||
<Text c={dark ? 'white' : 'darmasaba-navy'}>{agenda.event}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed" ta="center" py="md">
|
||||
Tidak ada acara hari ini
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
{/* SECTION 4 — ACARA HARI INI */}
|
||||
<EventCard />
|
||||
|
||||
{/* SECTION 5 — ARSIP DIGITAL PERANGKAT DESA */}
|
||||
<Grid gutter="md">
|
||||
{archiveData.map((item, index) => (
|
||||
<Grid.Col key={index} span={{ base: 12, md: 6 }}>
|
||||
<ArchiveCard item={item} />
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default KinerjaDivisi;
|
||||
export default KinerjaDivisi;
|
||||
|
||||
89
src/components/kinerja-divisi/activity-card.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Card, Text, Progress, Group, Box } from "@mantine/core";
|
||||
|
||||
interface ActivityCardProps {
|
||||
title: string;
|
||||
date: string;
|
||||
progress: number;
|
||||
status: "Selesai" | "Berjalan" | "Tertunda";
|
||||
}
|
||||
|
||||
export function ActivityCard({
|
||||
title,
|
||||
date,
|
||||
progress,
|
||||
status,
|
||||
}: ActivityCardProps) {
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case "Selesai":
|
||||
return "#22C55E";
|
||||
case "Berjalan":
|
||||
return "#3B82F6";
|
||||
case "Tertunda":
|
||||
return "#EF4444";
|
||||
default:
|
||||
return "#9CA3AF";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
radius="xl"
|
||||
p={0}
|
||||
withBorder={false}
|
||||
style={{
|
||||
backgroundColor: "#F3F4F6",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 🔵 HEADER */}
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: "#1E3A5F",
|
||||
padding: "16px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Text c="white" fw={700} size="md">
|
||||
{title}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* CONTENT */}
|
||||
<Box p="md">
|
||||
{/* PROGRESS */}
|
||||
<Progress
|
||||
value={progress}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
color="orange"
|
||||
styles={{
|
||||
root: {
|
||||
height: 16,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* FOOTER */}
|
||||
<Group justify="space-between" mt="md">
|
||||
<Text size="sm" fw={500}>
|
||||
{date}
|
||||
</Text>
|
||||
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: getStatusColor(),
|
||||
color: "white",
|
||||
padding: "4px 12px",
|
||||
borderRadius: 999,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</Box>
|
||||
</Group>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
42
src/components/kinerja-divisi/archive-card.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Card, Group, Text, useMantineColorScheme } from "@mantine/core";
|
||||
import { FileText } from "lucide-react";
|
||||
|
||||
interface ArchiveItem {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface ArchiveCardProps {
|
||||
item: ArchiveItem;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export function ArchiveCard({ item, onClick }: ArchiveCardProps) {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
cursor: "pointer",
|
||||
transition: "transform 0.2s, box-shadow 0.2s",
|
||||
}}
|
||||
h="100%"
|
||||
onClick={onClick}
|
||||
>
|
||||
<Group gap="md">
|
||||
<FileText size={32} color={dark ? "#60A5FA" : "#3B82F6"} />
|
||||
<Text size="sm" fw={500} c={dark ? "white" : "#1E3A5F"}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
93
src/components/kinerja-divisi/discussion-panel.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { MessageCircle } from "lucide-react";
|
||||
|
||||
interface DiscussionItem {
|
||||
message: string;
|
||||
sender: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const discussions: DiscussionItem[] = [
|
||||
{
|
||||
message: "Kepada Pelayanan, mohon di cek...",
|
||||
sender: "I.B Surya Prabhawa Manu",
|
||||
date: "12 Apr 2025",
|
||||
},
|
||||
{
|
||||
message: "Kepada staf perencanaan @suar...",
|
||||
sender: "Ni Nyoman Yuliani",
|
||||
date: "14 Jun 2025",
|
||||
},
|
||||
{
|
||||
message: "ijin atau mohon kepada KBD sar...",
|
||||
sender: "Ni Wayan Martini",
|
||||
date: "12 Apr 2025",
|
||||
},
|
||||
];
|
||||
|
||||
export function DiscussionPanel() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group gap="xs" mb="md">
|
||||
<MessageCircle size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||
Diskusi
|
||||
</Text>
|
||||
</Group>
|
||||
<Stack gap="sm">
|
||||
{discussions.map((discussion, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
p="sm"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#334155" : "#F1F5F9"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "#F1F5F9",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
size="sm"
|
||||
c={dark ? "white" : "#1E3A5F"}
|
||||
fw={500}
|
||||
mb="xs"
|
||||
lineClamp={2}
|
||||
>
|
||||
{discussion.message}
|
||||
</Text>
|
||||
<Group justify="space-between">
|
||||
<Text size="xs" c="dimmed">
|
||||
{discussion.sender}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{discussion.date}
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
77
src/components/kinerja-divisi/division-list.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
interface DivisionItem {
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
const divisionData: DivisionItem[] = [
|
||||
{ name: "Kesejahteraan", count: 37 },
|
||||
{ name: "Pemerintahan", count: 26 },
|
||||
{ name: "Keuangan", count: 17 },
|
||||
{ name: "Sekretaris Desa", count: 15 },
|
||||
{ name: "Tata Usaha TK", count: 14 },
|
||||
{ name: "Perangkat Kewilayahan", count: 12 },
|
||||
{ name: "Pelayanan", count: 10 },
|
||||
{ name: "Perencanaan", count: 9 },
|
||||
{ name: "Tata Usaha & Umum", count: 7 },
|
||||
];
|
||||
|
||||
export function DivisionList() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
|
||||
Divisi Teraktif
|
||||
</Text>
|
||||
<Stack gap="xs">
|
||||
{divisionData.map((division, index) => (
|
||||
<Group
|
||||
key={index}
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
borderRadius: 8,
|
||||
backgroundColor: dark ? "#334155" : "#F1F5F9",
|
||||
transition: "background-color 0.2s",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<Text size="sm" c={dark ? "white" : "#1E3A5F"}>
|
||||
{division.name}
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||
{division.count}
|
||||
</Text>
|
||||
<ChevronRight size={16} color={dark ? "#94A3B8" : "#64748B"} />
|
||||
</Group>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
74
src/components/kinerja-divisi/document-chart.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Card, Text, useMantineColorScheme } from "@mantine/core";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
const documentData = [
|
||||
{ name: "Gambar", jumlah: 300, color: "#FACC15" },
|
||||
{ name: "Dokumen", jumlah: 310, color: "#22C55E" },
|
||||
];
|
||||
|
||||
export function DocumentChart() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
|
||||
Jumlah Dokumen
|
||||
</Text>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<BarChart data={documentData}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke={dark ? "#334155" : "#e5e7eb"}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Bar dataKey="jumlah" radius={[4, 4, 0, 0]}>
|
||||
{documentData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
66
src/components/kinerja-divisi/event-card.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { Calendar } from "lucide-react";
|
||||
|
||||
interface AgendaItem {
|
||||
time: string;
|
||||
event: string;
|
||||
}
|
||||
|
||||
interface EventCardProps {
|
||||
agendas?: AgendaItem[];
|
||||
}
|
||||
|
||||
export function EventCard({ agendas = [] }: EventCardProps) {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group gap="xs" mb="md">
|
||||
<Calendar size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||
Acara Hari Ini
|
||||
</Text>
|
||||
</Group>
|
||||
{agendas.length > 0 ? (
|
||||
<Stack gap="sm">
|
||||
{agendas.map((agenda, index) => (
|
||||
<Group key={index} align="flex-start" gap="md">
|
||||
<Box w={60}>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||
{agenda.time}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text size="sm" c={dark ? "white" : "#1E3A5F"}>
|
||||
{agenda.event}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed" ta="center" py="md">
|
||||
Tidak ada acara hari ini
|
||||
</Text>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
7
src/components/kinerja-divisi/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { ActivityCard } from "./activity-card";
|
||||
export { ArchiveCard } from "./archive-card";
|
||||
export { DiscussionPanel } from "./discussion-panel";
|
||||
export { DivisionList } from "./division-list";
|
||||
export { DocumentChart } from "./document-chart";
|
||||
export { EventCard } from "./event-card";
|
||||
export { ProgressChart } from "./progress-chart";
|
||||
84
src/components/kinerja-divisi/progress-chart.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
|
||||
|
||||
const progressData = [
|
||||
{ name: "Selesai", value: 83.33, color: "#22C55E" },
|
||||
{ name: "Dikerjakan", value: 16.67, color: "#F59E0B" },
|
||||
{ name: "Segera Dikerjakan", value: 0, color: "#3B82F6" },
|
||||
{ name: "Dibatalkan", value: 0, color: "#EF4444" },
|
||||
];
|
||||
|
||||
export function ProgressChart() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : "white",
|
||||
boxShadow: dark
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"} mb="md">
|
||||
Progres Kegiatan
|
||||
</Text>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={progressData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={80}
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{progressData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: dark ? "#1E293B" : "white",
|
||||
borderColor: dark ? "#334155" : "#e5e7eb",
|
||||
borderRadius: "8px",
|
||||
}}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<Stack gap="xs" mt="md">
|
||||
{progressData.map((item, index) => (
|
||||
<Group key={index} justify="space-between">
|
||||
<Group gap="xs">
|
||||
<Box
|
||||
w={12}
|
||||
h={12}
|
||||
style={{ backgroundColor: item.color, borderRadius: 2 }}
|
||||
/>
|
||||
<Text size="sm" c={dark ? "white" : "gray.7"}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
|
||||
{item.value.toFixed(2)}%
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,54 @@
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Select,
|
||||
Table,
|
||||
Badge,
|
||||
Stack,
|
||||
useMantineColorScheme,
|
||||
List,
|
||||
Divider,
|
||||
ActionIcon,
|
||||
Box
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconMessage, IconAlertTriangle, IconClock, IconCheck, IconChevronRight } from "@tabler/icons-react";
|
||||
import { Line, LineChart, Bar, BarChart, CartesianGrid, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts";
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconCheck,
|
||||
IconChevronRight,
|
||||
IconClock,
|
||||
IconMessage,
|
||||
} from "@tabler/icons-react";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
|
||||
const PengaduanLayananPublik = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
// Summary data
|
||||
const summaryData = {
|
||||
total: 42,
|
||||
baru: 14,
|
||||
diproses: 14,
|
||||
selesai: 14
|
||||
selesai: 14,
|
||||
};
|
||||
|
||||
// Tren pengaduan data
|
||||
@@ -42,7 +58,7 @@ const PengaduanLayananPublik = () => {
|
||||
{ bulan: "Mar", jumlah: 42 },
|
||||
{ bulan: "Apr", jumlah: 38 },
|
||||
{ bulan: "Mei", jumlah: 45 },
|
||||
{ bulan: "Jun", jumlah: 42 }
|
||||
{ bulan: "Jun", jumlah: 42 },
|
||||
];
|
||||
|
||||
// Surat terbanyak data
|
||||
@@ -51,24 +67,65 @@ const PengaduanLayananPublik = () => {
|
||||
{ jenis: "KK", jumlah: 18 },
|
||||
{ jenis: "Domisili", jumlah: 15 },
|
||||
{ jenis: "Usaha", jumlah: 12 },
|
||||
{ jenis: "Lainnya", jumlah: 8 }
|
||||
{ jenis: "Lainnya", jumlah: 8 },
|
||||
];
|
||||
|
||||
// Pengajuan terbaru data
|
||||
const pengajuanTerbaru = [
|
||||
{ nama: "Budi Santoso", jenis: "Ketertiban Umum", waktu: "2 jam yang lalu", status: "baru" },
|
||||
{ nama: "Siti Rahayu", jenis: "Pelayanan Kesehatan", waktu: "5 jam yang lalu", status: "diproses" },
|
||||
{ nama: "Ahmad Fauzi", jenis: "Infrastruktur", waktu: "1 hari yang lalu", status: "selesai" },
|
||||
{ nama: "Dewi Lestari", jenis: "Administrasi", waktu: "1 hari yang lalu", status: "baru" },
|
||||
{ nama: "Joko Widodo", jenis: "Keamanan", waktu: "2 hari yang lalu", status: "diproses" }
|
||||
{
|
||||
nama: "Budi Santoso",
|
||||
jenis: "Ketertiban Umum",
|
||||
waktu: "2 jam yang lalu",
|
||||
status: "baru",
|
||||
},
|
||||
{
|
||||
nama: "Siti Rahayu",
|
||||
jenis: "Pelayanan Kesehatan",
|
||||
waktu: "5 jam yang lalu",
|
||||
status: "diproses",
|
||||
},
|
||||
{
|
||||
nama: "Ahmad Fauzi",
|
||||
jenis: "Infrastruktur",
|
||||
waktu: "1 hari yang lalu",
|
||||
status: "selesai",
|
||||
},
|
||||
{
|
||||
nama: "Dewi Lestari",
|
||||
jenis: "Administrasi",
|
||||
waktu: "1 hari yang lalu",
|
||||
status: "baru",
|
||||
},
|
||||
{
|
||||
nama: "Joko Widodo",
|
||||
jenis: "Keamanan",
|
||||
waktu: "2 hari yang lalu",
|
||||
status: "diproses",
|
||||
},
|
||||
];
|
||||
|
||||
// Ide inovatif data
|
||||
const ideInovatif = [
|
||||
{ nama: "Andi Prasetyo", judul: "Penerapan Smart Village", kategori: "Teknologi" },
|
||||
{ nama: "Rina Kusuma", judul: "Program Ekowisata Desa", kategori: "Ekonomi" },
|
||||
{ nama: "Bambang Suryono", judul: "Peningkatan Sanitasi", kategori: "Kesehatan" },
|
||||
{ nama: "Lina Marlina", judul: "Pusat Kreatif Anak Muda", kategori: "Pendidikan" }
|
||||
{
|
||||
nama: "Andi Prasetyo",
|
||||
judul: "Penerapan Smart Village",
|
||||
kategori: "Teknologi",
|
||||
},
|
||||
{
|
||||
nama: "Rina Kusuma",
|
||||
judul: "Program Ekowisata Desa",
|
||||
kategori: "Ekonomi",
|
||||
},
|
||||
{
|
||||
nama: "Bambang Suryono",
|
||||
judul: "Peningkatan Sanitasi",
|
||||
kategori: "Kesehatan",
|
||||
},
|
||||
{
|
||||
nama: "Lina Marlina",
|
||||
judul: "Pusat Kreatif Anak Muda",
|
||||
kategori: "Pendidikan",
|
||||
},
|
||||
];
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"complaints" | "services">(
|
||||
@@ -229,10 +286,14 @@ const PengaduanLayananPublik = () => {
|
||||
// Status badge color mapping
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'baru': return 'red';
|
||||
case 'diproses': return 'yellow';
|
||||
case 'selesai': return 'green';
|
||||
default: return 'gray';
|
||||
case "baru":
|
||||
return "red";
|
||||
case "diproses":
|
||||
return "yellow";
|
||||
case "selesai":
|
||||
return "green";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -243,7 +304,14 @@ const PengaduanLayananPublik = () => {
|
||||
{/* Summary Cards */}
|
||||
<Grid gutter="md">
|
||||
<GridCol span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
@@ -266,7 +334,14 @@ const PengaduanLayananPublik = () => {
|
||||
</GridCol>
|
||||
|
||||
<GridCol span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
@@ -276,12 +351,7 @@ const PengaduanLayananPublik = () => {
|
||||
{summaryData.baru}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Badge
|
||||
variant="light"
|
||||
color="red"
|
||||
p={8}
|
||||
radius="md"
|
||||
>
|
||||
<Badge variant="light" color="red" p={8} radius="md">
|
||||
<IconAlertTriangle size={20} />
|
||||
</Badge>
|
||||
</Group>
|
||||
@@ -289,7 +359,14 @@ const PengaduanLayananPublik = () => {
|
||||
</GridCol>
|
||||
|
||||
<GridCol span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
@@ -299,12 +376,7 @@ const PengaduanLayananPublik = () => {
|
||||
{summaryData.diproses}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Badge
|
||||
variant="light"
|
||||
color="yellow"
|
||||
p={8}
|
||||
radius="md"
|
||||
>
|
||||
<Badge variant="light" color="yellow" p={8} radius="md">
|
||||
<IconClock size={20} />
|
||||
</Badge>
|
||||
</Group>
|
||||
@@ -312,7 +384,14 @@ const PengaduanLayananPublik = () => {
|
||||
</GridCol>
|
||||
|
||||
<GridCol span={{ base: 12, md: 6, lg: 3 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
<Stack gap={0}>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
@@ -322,12 +401,7 @@ const PengaduanLayananPublik = () => {
|
||||
{summaryData.selesai}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Badge
|
||||
variant="light"
|
||||
color="green"
|
||||
p={8}
|
||||
radius="md"
|
||||
>
|
||||
<Badge variant="light" color="green" p={8} radius="md">
|
||||
<IconCheck size={20} />
|
||||
</Badge>
|
||||
</Group>
|
||||
@@ -336,7 +410,13 @@ const PengaduanLayananPublik = () => {
|
||||
</Grid>
|
||||
|
||||
{/* Grafik Tren Pengaduan */}
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} >
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
|
||||
Grafik Tren Pengaduan
|
||||
</Title>
|
||||
@@ -345,31 +425,58 @@ const PengaduanLayananPublik = () => {
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke={dark ? "var(--mantine-color-gray-7)" : "var(--mantine-color-gray-3)"}
|
||||
stroke={
|
||||
dark
|
||||
? "var(--mantine-color-gray-7)"
|
||||
: "var(--mantine-color-gray-3)"
|
||||
}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="bulan"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
|
||||
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)" }}
|
||||
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)' }
|
||||
: {}}
|
||||
contentStyle={
|
||||
dark
|
||||
? {
|
||||
backgroundColor: "var(--mantine-color-dark-7)",
|
||||
borderColor: "var(--mantine-color-dark-6)",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="jumlah"
|
||||
stroke={dark ? "var(--mantine-color-blue-6)" : "var(--mantine-color-blue-filled)"}
|
||||
stroke={
|
||||
dark
|
||||
? "var(--mantine-color-blue-6)"
|
||||
: "var(--mantine-color-blue-filled)"
|
||||
}
|
||||
strokeWidth={2}
|
||||
dot={{ stroke: dark ? "var(--mantine-color-blue-6)" : "var(--mantine-color-blue-filled)", strokeWidth: 2, r: 4 }}
|
||||
activeDot={{ r: 6, stroke: '#fff', strokeWidth: 2 }}
|
||||
dot={{
|
||||
stroke: dark
|
||||
? "var(--mantine-color-blue-6)"
|
||||
: "var(--mantine-color-blue-filled)",
|
||||
strokeWidth: 2,
|
||||
r: 4,
|
||||
}}
|
||||
activeDot={{ r: 6, stroke: "#fff", strokeWidth: 2 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -379,7 +486,14 @@ const PengaduanLayananPublik = () => {
|
||||
<Grid gutter="md">
|
||||
{/* Surat Terbanyak */}
|
||||
<GridCol span={{ base: 12, lg: 4 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
|
||||
Surat Terbanyak
|
||||
</Title>
|
||||
@@ -388,30 +502,51 @@ const PengaduanLayananPublik = () => {
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
horizontal={false}
|
||||
stroke={dark ? "var(--mantine-color-gray-7)" : "var(--mantine-color-gray-3)"}
|
||||
stroke={
|
||||
dark
|
||||
? "var(--mantine-color-gray-7)"
|
||||
: "var(--mantine-color-gray-3)"
|
||||
}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="jumlah"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
|
||||
tick={{
|
||||
fill: dark
|
||||
? "var(--mantine-color-text)"
|
||||
: "var(--mantine-color-text)",
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="jenis"
|
||||
type="category"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fill: dark ? "var(--mantine-color-text)" : "var(--mantine-color-text)" }}
|
||||
tick={{
|
||||
fill: dark
|
||||
? "var(--mantine-color-text)"
|
||||
: "var(--mantine-color-text)",
|
||||
}}
|
||||
width={80}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={dark
|
||||
? { backgroundColor: 'var(--mantine-color-dark-7)', borderColor: 'var(--mantine-color-dark-6)' }
|
||||
: {}}
|
||||
contentStyle={
|
||||
dark
|
||||
? {
|
||||
backgroundColor: "var(--mantine-color-dark-7)",
|
||||
borderColor: "var(--mantine-color-dark-6)",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="jumlah"
|
||||
fill={dark ? "var(--mantine-color-blue-6)" : "var(--mantine-color-blue-filled)"}
|
||||
fill={
|
||||
dark
|
||||
? "var(--mantine-color-blue-6)"
|
||||
: "var(--mantine-color-blue-filled)"
|
||||
}
|
||||
radius={[0, 4, 4, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
@@ -421,7 +556,14 @@ const PengaduanLayananPublik = () => {
|
||||
|
||||
{/* Pengajuan Terbaru */}
|
||||
<GridCol span={{ base: 12, lg: 4 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
|
||||
Pengajuan Terbaru
|
||||
</Title>
|
||||
@@ -429,14 +571,23 @@ const PengaduanLayananPublik = () => {
|
||||
<Box key={index}>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>{item.nama}</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{item.jenis}</Text>
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{item.jenis}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack gap={0} align="flex-end">
|
||||
<Badge color={getStatusColor(item.status)} variant="light">
|
||||
<Badge
|
||||
color={getStatusColor(item.status)}
|
||||
variant="light"
|
||||
>
|
||||
{item.status}
|
||||
</Badge>
|
||||
<Text size="xs" c={dark ? "dark.4" : "dimmed"}>{item.waktu}</Text>
|
||||
<Text size="xs" c={dark ? "dark.4" : "dimmed"}>
|
||||
{item.waktu}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Divider my="sm" />
|
||||
@@ -447,7 +598,14 @@ const PengaduanLayananPublik = () => {
|
||||
|
||||
{/* Ajuan Ide Inovatif */}
|
||||
<GridCol span={{ base: 12, lg: 4 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} mb="md" c={dark ? "dark.0" : "black"}>
|
||||
Ajuan Ide Inovatif
|
||||
</Title>
|
||||
@@ -455,8 +613,12 @@ const PengaduanLayananPublik = () => {
|
||||
<Box key={index}>
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>{item.judul}</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{item.nama}</Text>
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
{item.judul}
|
||||
</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{item.nama}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Group>
|
||||
<Badge color="blue" variant="light">
|
||||
@@ -478,9 +640,18 @@ const PengaduanLayananPublik = () => {
|
||||
<Grid gutter="md">
|
||||
{/* Complaint Submission Form */}
|
||||
<GridCol span={{ base: 12, lg: 4 }}>
|
||||
<Card p="md" withBorder radius="md" h="100%" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
p="md"
|
||||
withBorder
|
||||
radius="md"
|
||||
h="100%"
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Title order={3} py="xs">Ajukan Pengaduan</Title>
|
||||
<Title order={3} py="xs">
|
||||
Ajukan Pengaduan
|
||||
</Title>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<form onSubmit={handleSubmitComplaint}>
|
||||
@@ -537,24 +708,39 @@ const PengaduanLayananPublik = () => {
|
||||
|
||||
{/* Complaints List */}
|
||||
<GridCol span={{ base: 12, lg: 8 }}>
|
||||
<Card withBorder radius="md" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Title order={3} py="xs">Daftar Pengaduan</Title>
|
||||
<Title order={3} py="xs">
|
||||
Daftar Pengaduan
|
||||
</Title>
|
||||
</Card.Section>
|
||||
<Card.Section py="md" px="xs">
|
||||
<Table withColumnBorders>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th><Text c={dark ? "white" : "dark.3" }>Judul</Text></Table.Th>
|
||||
<Table.Th><Text c={dark ? "white" : "dark.3" }>Kategori</Text></Table.Th>
|
||||
<Table.Th><Text c={dark ? "white" : "dark.3" }>Status</Text></Table.Th>
|
||||
<Table.Th><Text c={dark ? "white" : "dark.3" }>Prioritas</Text></Table.Th>
|
||||
<Table.Th><Text c={dark ? "white" : "dark.3" }>Tanggal</Text></Table.Th>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dark.3"}>Judul</Text>
|
||||
</Table.Th>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dark.3"}>Kategori</Text>
|
||||
</Table.Th>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dark.3"}>Status</Text>
|
||||
</Table.Th>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dark.3"}>Prioritas</Text>
|
||||
</Table.Th>
|
||||
<Table.Th>
|
||||
<Text c={dark ? "white" : "dark.3"}>Tanggal</Text>
|
||||
</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{complaintRows}
|
||||
</Table.Tbody>
|
||||
<Table.Tbody>{complaintRows}</Table.Tbody>
|
||||
</Table>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
@@ -565,15 +751,19 @@ const PengaduanLayananPublik = () => {
|
||||
<Stack gap="lg">
|
||||
<Card withBorder radius="md">
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Title order={3} py="xs">Layanan Publik Tersedia</Title>
|
||||
<Title order={3} py="xs">
|
||||
Layanan Publik Tersedia
|
||||
</Title>
|
||||
</Card.Section>
|
||||
<Card.Section pt="md">
|
||||
<Grid gutter="md">
|
||||
{services.map((service) => (
|
||||
<GridCol key={service.id} span={{ base: 12, md: 6, lg: 4 }}>
|
||||
<Card withBorder radius="md" h="100%">
|
||||
<Title order={4} mb="sm">{service.name}</Title>
|
||||
<Text size="sm" c={dark ? "white" : "dark.3" } mb="md">
|
||||
<Title order={4} mb="sm">
|
||||
{service.name}
|
||||
</Title>
|
||||
<Text size="sm" c={dark ? "white" : "dark.3"} mb="md">
|
||||
{service.description}
|
||||
</Text>
|
||||
<Group justify="space-between">
|
||||
@@ -589,11 +779,11 @@ const PengaduanLayananPublik = () => {
|
||||
>
|
||||
{service.status}
|
||||
</Badge>
|
||||
<Text size="sm" c={dark ? "white" : "dark.3" }>
|
||||
<Text size="sm" c={dark ? "white" : "dark.3"}>
|
||||
{service.category}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c={dark ? "white" : "dark.3" } mt="sm">
|
||||
<Text size="xs" c={dark ? "white" : "dark.3"} mt="sm">
|
||||
Terakhir diperbarui: {service.lastUpdated}
|
||||
</Text>
|
||||
</Card>
|
||||
@@ -605,13 +795,17 @@ const PengaduanLayananPublik = () => {
|
||||
|
||||
<Card withBorder radius="md">
|
||||
<Card.Section withBorder inheritPadding py="xs">
|
||||
<Title order={3} py="xs">Statistik Layanan</Title>
|
||||
<Title order={3} py="xs">
|
||||
Statistik Layanan
|
||||
</Title>
|
||||
</Card.Section>
|
||||
<Card.Section pt="md">
|
||||
<Grid gutter="md">
|
||||
<GridCol span={{ base: 12, md: 4 }}>
|
||||
<Card p="md" bg={dark ? "dark.7" : "gray.0"} radius="md">
|
||||
<Title order={4} mb="xs">Jumlah Layanan Tersedia</Title>
|
||||
<Title order={4} mb="xs">
|
||||
Jumlah Layanan Tersedia
|
||||
</Title>
|
||||
<Text size="xl" fw={700} c="darmasaba-blue">
|
||||
12
|
||||
</Text>
|
||||
@@ -619,7 +813,9 @@ const PengaduanLayananPublik = () => {
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 4 }}>
|
||||
<Card p="md" bg={dark ? "dark.7" : "gray.0"} radius="md">
|
||||
<Title order={4} mb="xs">Layanan Terpopuler</Title>
|
||||
<Title order={4} mb="xs">
|
||||
Layanan Terpopuler
|
||||
</Title>
|
||||
<Text size="xl" fw={700} c="darmasaba-success">
|
||||
4
|
||||
</Text>
|
||||
@@ -627,7 +823,9 @@ const PengaduanLayananPublik = () => {
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 4 }}>
|
||||
<Card p="md" bg={dark ? "dark.7" : "gray.0"} radius="md">
|
||||
<Title order={4} mb="xs">Permintaan Baru</Title>
|
||||
<Title order={4} mb="xs">
|
||||
Permintaan Baru
|
||||
</Title>
|
||||
<Text size="xl" fw={700} c="darmasaba-warning">
|
||||
23
|
||||
</Text>
|
||||
@@ -642,4 +840,4 @@ const PengaduanLayananPublik = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default PengaduanLayananPublik;
|
||||
export default PengaduanLayananPublik;
|
||||
|
||||
@@ -1,125 +1,190 @@
|
||||
import { Card, Title, Text, Space, Button, Group, Alert, Table, ActionIcon, Modal, TextInput, Select, useMantineColorScheme } from '@mantine/core';
|
||||
import { IconInfoCircle, IconUserPlus, IconTrash, IconEdit, IconUser } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
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 [opened, setOpened] = useState(false);
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
const [opened, setOpened] = useState(false);
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
// Sample team members data
|
||||
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: 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' },
|
||||
];
|
||||
// Sample team members data
|
||||
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: 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 = [
|
||||
{ value: 'administrator', label: 'Administrator' },
|
||||
{ value: 'operator', label: 'Operator' },
|
||||
{ value: 'keuangan', label: 'Keuangan' },
|
||||
{ value: 'umum', label: 'Umum' },
|
||||
{ value: 'keamanan', label: 'Keamanan' },
|
||||
];
|
||||
const roles = [
|
||||
{ value: "administrator", label: "Administrator" },
|
||||
{ value: "operator", label: "Operator" },
|
||||
{ value: "keuangan", label: "Keuangan" },
|
||||
{ value: "umum", label: "Umum" },
|
||||
{ value: "keamanan", label: "Keamanan" },
|
||||
];
|
||||
|
||||
return (
|
||||
<Card withBorder radius="md" p="xl" bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title="Tambah Anggota Tim"
|
||||
size="lg"
|
||||
>
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan nama lengkap anggota tim"
|
||||
mb="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Alamat Email"
|
||||
placeholder="Masukkan alamat email"
|
||||
mb="md"
|
||||
/>
|
||||
<Select
|
||||
label="Peran"
|
||||
placeholder="Pilih peran anggota tim"
|
||||
data={roles}
|
||||
mb="md"
|
||||
/>
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline" onClick={() => setOpened(false)}>Batal</Button>
|
||||
<Button>Undang Anggota</Button>
|
||||
</Group>
|
||||
</Modal>
|
||||
return (
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
p="xl"
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title="Tambah Anggota Tim"
|
||||
size="lg"
|
||||
>
|
||||
<TextInput
|
||||
label="Nama Lengkap"
|
||||
placeholder="Masukkan nama lengkap anggota tim"
|
||||
mb="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Alamat Email"
|
||||
placeholder="Masukkan alamat email"
|
||||
mb="md"
|
||||
/>
|
||||
<Select
|
||||
label="Peran"
|
||||
placeholder="Pilih peran anggota tim"
|
||||
data={roles}
|
||||
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>
|
||||
<Text color="dimmed" mb="xl">Kelola akses dan anggota tim Anda</Text>
|
||||
<Title order={2} mb="lg">
|
||||
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">
|
||||
<Title order={4}>Anggota Tim</Title>
|
||||
<Button leftSection={<IconUserPlus size={16} />} onClick={() => setOpened(true)}>
|
||||
Tambah Anggota
|
||||
</Button>
|
||||
</Group>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Title order={4}>Anggota Tim</Title>
|
||||
<Button
|
||||
leftSection={<IconUserPlus size={16} />}
|
||||
onClick={() => setOpened(true)}
|
||||
>
|
||||
Tambah Anggota
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Nama</Table.Th>
|
||||
<Table.Th>Email</Table.Th>
|
||||
<Table.Th>Peran</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{teamMembers.map((member) => (
|
||||
<Table.Tr key={member.id}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<IconUser size={20} />
|
||||
<Text>{member.name}</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>{member.email}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fw={500}>{member.role}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text c={member.status === 'Aktif' ? 'green' : 'red'} fw={500}>
|
||||
{member.status}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<ActionIcon variant="subtle" color="blue">
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="subtle" color="red">
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Nama</Table.Th>
|
||||
<Table.Th>Email</Table.Th>
|
||||
<Table.Th>Peran</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{teamMembers.map((member) => (
|
||||
<Table.Tr key={member.id}>
|
||||
<Table.Td>
|
||||
<Group gap="sm">
|
||||
<IconUser size={20} />
|
||||
<Text>{member.name}</Text>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>{member.email}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fw={500}>{member.role}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text c={member.status === "Aktif" ? "green" : "red"} fw={500}>
|
||||
{member.status}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<ActionIcon variant="subtle" color="blue">
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon variant="subtle" color="red">
|
||||
<IconTrash size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
|
||||
<Space h="xl" />
|
||||
<Space h="xl" />
|
||||
|
||||
<Alert icon={<IconInfoCircle size={16} />} title="Informasi" color="blue" mb="md">
|
||||
Administrator memiliki akses penuh ke semua fitur. Peran lainnya memiliki akses terbatas sesuai kebutuhan.
|
||||
</Alert>
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
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">
|
||||
<Button variant="outline">Batal</Button>
|
||||
<Button>Simpan Perubahan</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline">Batal</Button>
|
||||
<Button>Simpan Perubahan</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AksesDanTimSettings;
|
||||
export default AksesDanTimSettings;
|
||||
|
||||
@@ -1,57 +1,90 @@
|
||||
import { Card, Title, Text, Space, Button, Group, Alert, PasswordInput, Switch, useMantineColorScheme } from '@mantine/core';
|
||||
import { IconInfoCircle, IconLock } from '@tabler/icons-react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
PasswordInput,
|
||||
Space,
|
||||
Switch,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconInfoCircle, IconLock } from "@tabler/icons-react";
|
||||
|
||||
const KeamananSettings = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
return (
|
||||
<Card withBorder 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>
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
return (
|
||||
<Card
|
||||
withBorder
|
||||
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
|
||||
label="Kata Sandi Saat Ini"
|
||||
placeholder="Masukkan kata sandi saat ini"
|
||||
mb="md"
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Kata Sandi Saat Ini"
|
||||
placeholder="Masukkan kata sandi saat ini"
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Kata Sandi Baru"
|
||||
placeholder="Masukkan kata sandi baru"
|
||||
mb="md"
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Kata Sandi Baru"
|
||||
placeholder="Masukkan kata sandi baru"
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Konfirmasi Kata Sandi Baru"
|
||||
placeholder="Konfirmasi kata sandi baru"
|
||||
mb="md"
|
||||
/>
|
||||
<PasswordInput
|
||||
label="Konfirmasi Kata Sandi Baru"
|
||||
placeholder="Konfirmasi kata sandi baru"
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
<Space h="md" />
|
||||
<Space h="md" />
|
||||
|
||||
<Group mb="md">
|
||||
<Switch label="Verifikasi Dua Langkah" />
|
||||
<Switch label="Login Otentikasi Aplikasi" />
|
||||
</Group>
|
||||
<Group mb="md">
|
||||
<Switch label="Verifikasi Dua Langkah" />
|
||||
<Switch label="Login Otentikasi Aplikasi" />
|
||||
</Group>
|
||||
|
||||
<Space h="md" />
|
||||
<Space h="md" />
|
||||
|
||||
<Alert icon={<IconLock size={16} />} 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={<IconLock size={16} />}
|
||||
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">
|
||||
Setelah mengganti kata sandi, Anda akan diminta logout dari semua perangkat.
|
||||
</Alert>
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
title="Informasi"
|
||||
color="blue"
|
||||
mb="md"
|
||||
>
|
||||
Setelah mengganti kata sandi, Anda akan diminta logout dari semua
|
||||
perangkat.
|
||||
</Alert>
|
||||
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline">Batal</Button>
|
||||
<Button>Perbarui Kata Sandi</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline">Batal</Button>
|
||||
<Button>Perbarui Kata Sandi</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeamananSettings;
|
||||
export default KeamananSettings;
|
||||
|
||||
@@ -1,55 +1,86 @@
|
||||
import { Card, Title, Text, Space, Switch, Group, Alert, Checkbox, Button, useMantineColorScheme } from '@mantine/core';
|
||||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Group,
|
||||
Space,
|
||||
Switch,
|
||||
Text,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
|
||||
const NotifikasiSettings = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
return (
|
||||
<Card withBorder 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>
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
return (
|
||||
<Card
|
||||
withBorder
|
||||
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">
|
||||
<Title order={4} mb="sm">Metode Notifikasi</Title>
|
||||
<Group>
|
||||
<Checkbox value="email" label="Email" />
|
||||
<Checkbox value="push" label="Notifikasi Push" />
|
||||
<Checkbox value="sms" label="SMS" />
|
||||
</Group>
|
||||
</Checkbox.Group>
|
||||
<Checkbox.Group defaultValue={["email", "push"]} mb="md">
|
||||
<Title order={4} mb="sm">
|
||||
Metode Notifikasi
|
||||
</Title>
|
||||
<Group>
|
||||
<Checkbox value="email" label="Email" />
|
||||
<Checkbox value="push" label="Notifikasi Push" />
|
||||
<Checkbox value="sms" label="SMS" />
|
||||
</Group>
|
||||
</Checkbox.Group>
|
||||
|
||||
<Space h="md" />
|
||||
<Space h="md" />
|
||||
|
||||
<Group mb="md">
|
||||
<Switch label="Notifikasi Email" defaultChecked />
|
||||
<Switch label="Notifikasi Push" defaultChecked />
|
||||
</Group>
|
||||
<Group mb="md">
|
||||
<Switch label="Notifikasi Email" defaultChecked />
|
||||
<Switch label="Notifikasi Push" defaultChecked />
|
||||
</Group>
|
||||
|
||||
<Space h="md" />
|
||||
<Space h="md" />
|
||||
|
||||
<Title order={4} mb="sm">Jenis Notifikasi</Title>
|
||||
<Group align="start">
|
||||
<Switch label="Pengaduan Baru" defaultChecked />
|
||||
<Switch label="Update Status Pengaduan" defaultChecked />
|
||||
<Switch label="Laporan Mingguan" />
|
||||
<Switch label="Pemberitahuan Keamanan" defaultChecked />
|
||||
<Switch label="Aktivitas Akun" defaultChecked />
|
||||
</Group>
|
||||
<Title order={4} mb="sm">
|
||||
Jenis Notifikasi
|
||||
</Title>
|
||||
<Group align="start">
|
||||
<Switch label="Pengaduan Baru" defaultChecked />
|
||||
<Switch label="Update Status Pengaduan" defaultChecked />
|
||||
<Switch label="Laporan Mingguan" />
|
||||
<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">
|
||||
Anda dapat menyesuaikan frekuensi notifikasi mingguan sesuai kebutuhan Anda.
|
||||
</Alert>
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
title="Tip"
|
||||
color="blue"
|
||||
mb="md"
|
||||
>
|
||||
Anda dapat menyesuaikan frekuensi notifikasi mingguan sesuai kebutuhan
|
||||
Anda.
|
||||
</Alert>
|
||||
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline">Batal</Button>
|
||||
<Button>Simpan Preferensi</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline">Batal</Button>
|
||||
<Button>Simpan Preferensi</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotifikasiSettings;
|
||||
export default NotifikasiSettings;
|
||||
|
||||
@@ -1,58 +1,86 @@
|
||||
import { Card, Title, Text, Space, TextInput, Select, Button, Group, Switch, Alert, useMantineColorScheme } from '@mantine/core';
|
||||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
|
||||
const UmumSettings = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
return (
|
||||
<Card withBorder 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>
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
return (
|
||||
<Card
|
||||
withBorder
|
||||
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
|
||||
label="Nama Aplikasi"
|
||||
placeholder="Masukkan nama aplikasi"
|
||||
defaultValue="Dashboard Desa Plus"
|
||||
mb="md"
|
||||
/>
|
||||
<TextInput
|
||||
label="Nama Aplikasi"
|
||||
placeholder="Masukkan nama aplikasi"
|
||||
defaultValue="Dashboard Desa Plus"
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Bahasa Aplikasi"
|
||||
data={[
|
||||
{ value: 'id', label: 'Indonesia' },
|
||||
{ value: 'en', label: 'English' },
|
||||
]}
|
||||
defaultValue="id"
|
||||
mb="md"
|
||||
/>
|
||||
<Select
|
||||
label="Bahasa Aplikasi"
|
||||
data={[
|
||||
{ value: "id", label: "Indonesia" },
|
||||
{ value: "en", label: "English" },
|
||||
]}
|
||||
defaultValue="id"
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Zona Waktu"
|
||||
data={[
|
||||
{ value: 'Asia/Jakarta', label: 'Asia/Jakarta (GMT+7)' },
|
||||
{ value: 'Asia/Makassar', label: 'Asia/Makassar (GMT+8)' },
|
||||
{ value: 'Asia/Jayapura', label: 'Asia/Jayapura (GMT+9)' },
|
||||
]}
|
||||
defaultValue="Asia/Jakarta"
|
||||
mb="md"
|
||||
/>
|
||||
<Select
|
||||
label="Zona Waktu"
|
||||
data={[
|
||||
{ value: "Asia/Jakarta", label: "Asia/Jakarta (GMT+7)" },
|
||||
{ value: "Asia/Makassar", label: "Asia/Makassar (GMT+8)" },
|
||||
{ value: "Asia/Jayapura", label: "Asia/Jayapura (GMT+9)" },
|
||||
]}
|
||||
defaultValue="Asia/Jakarta"
|
||||
mb="md"
|
||||
/>
|
||||
|
||||
<Group mb="md">
|
||||
<Switch label="Notifikasi Email" defaultChecked />
|
||||
</Group>
|
||||
<Group mb="md">
|
||||
<Switch label="Notifikasi Email" defaultChecked />
|
||||
</Group>
|
||||
|
||||
<Alert icon={<IconInfoCircle size={16} />} title="Informasi" color="blue" mb="md">
|
||||
Beberapa pengaturan mungkin memerlukan restart aplikasi untuk diterapkan sepenuhnya.
|
||||
</Alert>
|
||||
<Alert
|
||||
icon={<IconInfoCircle size={16} />}
|
||||
title="Informasi"
|
||||
color="blue"
|
||||
mb="md"
|
||||
>
|
||||
Beberapa pengaturan mungkin memerlukan restart aplikasi untuk diterapkan
|
||||
sepenuhnya.
|
||||
</Alert>
|
||||
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline">Batal</Button>
|
||||
<Button>Simpan Perubahan</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
<Group justify="flex-end" mt="xl">
|
||||
<Button variant="outline">Batal</Button>
|
||||
<Button>Simpan Perubahan</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default UmumSettings;
|
||||
export default UmumSettings;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useNavigate, useLocation } from "@tanstack/react-router";
|
||||
import { Search, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import {
|
||||
Stack,
|
||||
Group,
|
||||
Text,
|
||||
Badge,
|
||||
Box,
|
||||
Collapse,
|
||||
Group,
|
||||
Image,
|
||||
Input,
|
||||
NavLink as MantineNavLink,
|
||||
Box,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineColorScheme,
|
||||
Collapse,
|
||||
} from "@mantine/core";
|
||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||
import { ChevronDown, ChevronUp, Search } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface SidebarProps {
|
||||
@@ -21,66 +22,46 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
const isActiveBg = colorScheme === 'dark' ? "#182949" : "#E6F0FF";
|
||||
const isActiveBorder = colorScheme === 'dark' ? "#00398D" : "#1F41AE";
|
||||
|
||||
const dark = colorScheme === "dark";
|
||||
const isActiveBg = colorScheme === "dark" ? "#182949" : "#E6F0FF";
|
||||
const isActiveBorder = colorScheme === "dark" ? "#00398D" : "#1F41AE";
|
||||
|
||||
// State for settings submenu collapse
|
||||
const [settingsOpen, setSettingsOpen] = useState(
|
||||
location.pathname.startsWith('/dashboard/pengaturan')
|
||||
location.pathname.startsWith("/pengaturan"),
|
||||
);
|
||||
|
||||
// Define menu items with their paths
|
||||
const menuItems = [
|
||||
{ name: "Beranda", path: "/dashboard" },
|
||||
{ name: "Kinerja Divisi", path: "/dashboard/kinerja-divisi" },
|
||||
{ name: "Pengaduan & Layanan Publik", path: "/dashboard/pengaduan-layanan-publik" },
|
||||
{ name: "Jenna Analytic", path: "/dashboard/jenna-analytic" },
|
||||
{ name: "Demografi & Kependudukan", path: "/dashboard/demografi-pekerjaan" },
|
||||
{ name: "Keuangan & Anggaran", path: "/dashboard/keuangan-anggaran" },
|
||||
{ name: "Bumdes & UMKM Desa", path: "/dashboard/bumdes" },
|
||||
{ name: "Sosial", path: "/dashboard/sosial" },
|
||||
{ name: "Keamanan", path: "/dashboard/keamanan" },
|
||||
{ name: "Bantuan", path: "/dashboard/bantuan" },
|
||||
{ name: "Beranda", path: "/" },
|
||||
{ name: "Kinerja Divisi", path: "/kinerja-divisi" },
|
||||
{ name: "Pengaduan & Layanan Publik", path: "/pengaduan-layanan-publik" },
|
||||
{ name: "Jenna Analytic", path: "/jenna-analytic" },
|
||||
{ name: "Demografi & Kependudukan", path: "/demografi-pekerjaan" },
|
||||
{ name: "Keuangan & Anggaran", path: "/keuangan-anggaran" },
|
||||
{ name: "Bumdes & UMKM Desa", path: "/bumdes" },
|
||||
{ name: "Sosial", path: "/sosial" },
|
||||
{ name: "Keamanan", path: "/keamanan" },
|
||||
{ name: "Bantuan", path: "/bantuan" },
|
||||
];
|
||||
|
||||
// Settings submenu items
|
||||
const settingsItems = [
|
||||
{ name: "Umum", path: "/dashboard/pengaturan/umum" },
|
||||
{ name: "Notifikasi", path: "/dashboard/pengaturan/notifikasi" },
|
||||
{ name: "Keamanan", path: "/dashboard/pengaturan/keamanan" },
|
||||
{ name: "Akses & Tim", path: "/dashboard/pengaturan/akses-dan-tim" },
|
||||
{ name: "Umum", path: "/pengaturan/umum" },
|
||||
{ name: "Notifikasi", path: "/pengaturan/notifikasi" },
|
||||
{ name: "Keamanan", path: "/pengaturan/keamanan" },
|
||||
{ name: "Akses & Tim", path: "/pengaturan/akses-dan-tim" },
|
||||
];
|
||||
|
||||
// Check if any settings submenu is active
|
||||
const isSettingsActive = settingsItems.some(item =>
|
||||
location.pathname === item.path
|
||||
const isSettingsActive = settingsItems.some(
|
||||
(item) => location.pathname === item.path,
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className={className}>
|
||||
{/* Logo */}
|
||||
<Box p="md" style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}>
|
||||
<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>
|
||||
<Image src="/logo-desa-plus.png" alt="Logo" />
|
||||
|
||||
{/* Search */}
|
||||
<Box p="md">
|
||||
@@ -112,7 +93,9 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
style={{
|
||||
background: isActive ? isActiveBg : "transparent",
|
||||
fontWeight: isActive ? "bold" : "normal",
|
||||
borderLeft: isActive ? `4px solid ${isActiveBorder}` : "4px solid transparent",
|
||||
borderLeft: isActive
|
||||
? `4px solid ${isActiveBorder}`
|
||||
: "4px solid transparent",
|
||||
borderRadius: "8px",
|
||||
transition: "all 200ms ease",
|
||||
margin: "2px 0",
|
||||
@@ -121,8 +104,8 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
body: {
|
||||
"&:hover": {
|
||||
background: "#F1F5F9",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -132,7 +115,9 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
<Box>
|
||||
<MantineNavLink
|
||||
onClick={() => setSettingsOpen(!settingsOpen)}
|
||||
rightSection={settingsOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||
rightSection={
|
||||
settingsOpen ? <ChevronUp size={16} /> : <ChevronDown size={16} />
|
||||
}
|
||||
label="Pengaturan"
|
||||
active={isSettingsActive}
|
||||
variant="subtle"
|
||||
@@ -140,7 +125,9 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
style={{
|
||||
background: isSettingsActive ? isActiveBg : "transparent",
|
||||
fontWeight: isSettingsActive ? "bold" : "normal",
|
||||
borderLeft: isSettingsActive ? `4px solid ${isActiveBorder}` : "4px solid transparent",
|
||||
borderLeft: isSettingsActive
|
||||
? `4px solid ${isActiveBorder}`
|
||||
: "4px solid transparent",
|
||||
borderRadius: "8px",
|
||||
transition: "all 200ms ease",
|
||||
margin: "2px 0",
|
||||
@@ -149,12 +136,16 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
body: {
|
||||
"&:hover": {
|
||||
background: "#F1F5F9",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<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) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
return (
|
||||
@@ -168,7 +159,9 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
style={{
|
||||
background: isActive ? isActiveBg : "transparent",
|
||||
fontWeight: isActive ? "bold" : "normal",
|
||||
borderLeft: isActive ? `4px solid ${isActiveBorder}` : "4px solid transparent",
|
||||
borderLeft: isActive
|
||||
? `4px solid ${isActiveBorder}`
|
||||
: "4px solid transparent",
|
||||
borderRadius: "8px",
|
||||
transition: "all 200ms ease",
|
||||
margin: "2px 0",
|
||||
@@ -177,8 +170,8 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
body: {
|
||||
"&:hover": {
|
||||
background: "#F1F5F9",
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,289 +1,465 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
Text,
|
||||
Title,
|
||||
Progress,
|
||||
Stack,
|
||||
useMantineColorScheme,
|
||||
Badge,
|
||||
List,
|
||||
ThemeIcon
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
Grid,
|
||||
GridCol,
|
||||
Group,
|
||||
List,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconHeartbeat,
|
||||
IconBabyCarriage,
|
||||
IconStethoscope,
|
||||
IconMedicalCross,
|
||||
IconSchool,
|
||||
IconBook,
|
||||
IconCalendarEvent,
|
||||
IconAward
|
||||
import {
|
||||
IconAward,
|
||||
IconBabyCarriage,
|
||||
IconBook,
|
||||
IconCalendarEvent,
|
||||
IconHeartbeat,
|
||||
IconMedicalCross,
|
||||
IconSchool,
|
||||
IconStethoscope,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const SosialPage = () => {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === 'dark';
|
||||
|
||||
// Sample data for health statistics
|
||||
const healthStats = {
|
||||
ibuHamil: 87,
|
||||
balita: 342,
|
||||
alertStunting: 12,
|
||||
posyanduAktif: 8,
|
||||
};
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
// Sample data for health progress
|
||||
const healthProgress = [
|
||||
{ label: "Imunisasi Lengkap", value: 92, color: "green" },
|
||||
{ label: "Pemeriksaan Rutin", value: 88, color: "blue" },
|
||||
{ label: "Gizi Baik", value: 86, color: "teal" },
|
||||
{ label: "Target Stunting", value: 14, color: "red" },
|
||||
];
|
||||
// Sample data for health statistics
|
||||
const healthStats = {
|
||||
ibuHamil: 87,
|
||||
balita: 342,
|
||||
alertStunting: 12,
|
||||
posyanduAktif: 8,
|
||||
};
|
||||
|
||||
// Sample data for posyandu schedule
|
||||
const posyanduSchedule = [
|
||||
{ nama: "Posyandu Mawar", tanggal: "Senin, 15 Feb 2026", jam: "08:00 - 11:00" },
|
||||
{ nama: "Posyandu Melati", tanggal: "Selasa, 16 Feb 2026", jam: "08:00 - 11:00" },
|
||||
{ nama: "Posyandu Dahlia", tanggal: "Rabu, 17 Feb 2026", jam: "08:00 - 11:00" },
|
||||
{ nama: "Posyandu Anggrek", tanggal: "Kamis, 18 Feb 2026", jam: "08:00 - 11:00" },
|
||||
];
|
||||
// Sample data for health progress
|
||||
const healthProgress = [
|
||||
{ label: "Imunisasi Lengkap", value: 92, color: "green" },
|
||||
{ label: "Pemeriksaan Rutin", value: 88, color: "blue" },
|
||||
{ label: "Gizi Baik", value: 86, color: "teal" },
|
||||
{ label: "Target Stunting", value: 14, color: "red" },
|
||||
];
|
||||
|
||||
// Sample data for education stats
|
||||
const educationStats = {
|
||||
siswa: {
|
||||
tk: 125,
|
||||
sd: 480,
|
||||
smp: 210,
|
||||
sma: 150,
|
||||
},
|
||||
sekolah: {
|
||||
jumlah: 8,
|
||||
guru: 42,
|
||||
}
|
||||
};
|
||||
// Sample data for posyandu schedule
|
||||
const posyanduSchedule = [
|
||||
{
|
||||
nama: "Posyandu Mawar",
|
||||
tanggal: "Senin, 15 Feb 2026",
|
||||
jam: "08:00 - 11:00",
|
||||
},
|
||||
{
|
||||
nama: "Posyandu Melati",
|
||||
tanggal: "Selasa, 16 Feb 2026",
|
||||
jam: "08:00 - 11:00",
|
||||
},
|
||||
{
|
||||
nama: "Posyandu Dahlia",
|
||||
tanggal: "Rabu, 17 Feb 2026",
|
||||
jam: "08:00 - 11:00",
|
||||
},
|
||||
{
|
||||
nama: "Posyandu Anggrek",
|
||||
tanggal: "Kamis, 18 Feb 2026",
|
||||
jam: "08:00 - 11:00",
|
||||
},
|
||||
];
|
||||
|
||||
// Sample data for scholarships
|
||||
const scholarshipData = {
|
||||
penerima: 45,
|
||||
dana: "Rp 1.200.000.000",
|
||||
tahunAjaran: "2025/2026",
|
||||
};
|
||||
// Sample data for education stats
|
||||
const educationStats = {
|
||||
siswa: {
|
||||
tk: 125,
|
||||
sd: 480,
|
||||
smp: 210,
|
||||
sma: 150,
|
||||
},
|
||||
sekolah: {
|
||||
jumlah: 8,
|
||||
guru: 42,
|
||||
},
|
||||
};
|
||||
|
||||
// Sample data for cultural events
|
||||
const culturalEvents = [
|
||||
{ nama: "Hari Kesaktian Pancasila", tanggal: "1 Oktober 2025", lokasi: "Balai Desa" },
|
||||
{ nama: "Festival Budaya Desa", tanggal: "20 Mei 2026", lokasi: "Lapangan Desa" },
|
||||
{ nama: "Perayaan HUT Desa", tanggal: "17 Agustus 2026", lokasi: "Balai Desa" },
|
||||
];
|
||||
// Sample data for scholarships
|
||||
const scholarshipData = {
|
||||
penerima: 45,
|
||||
dana: "Rp 1.200.000.000",
|
||||
tahunAjaran: "2025/2026",
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Health Statistics Cards */}
|
||||
<Grid gutter="md">
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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>
|
||||
// Sample data for cultural events
|
||||
const culturalEvents = [
|
||||
{
|
||||
nama: "Hari Kesaktian Pancasila",
|
||||
tanggal: "1 Oktober 2025",
|
||||
lokasi: "Balai Desa",
|
||||
},
|
||||
{
|
||||
nama: "Festival Budaya Desa",
|
||||
tanggal: "20 Mei 2026",
|
||||
lokasi: "Lapangan Desa",
|
||||
},
|
||||
{
|
||||
nama: "Perayaan HUT Desa",
|
||||
tanggal: "17 Agustus 2026",
|
||||
lokasi: "Balai Desa",
|
||||
},
|
||||
];
|
||||
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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"}>
|
||||
Balita Terdaftar
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{healthStats.balita}
|
||||
</Text>
|
||||
</Stack>
|
||||
<ThemeIcon variant="light" color="darmasaba-success" size="xl" radius="xl">
|
||||
<IconBabyCarriage size={24} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
{/* Health Statistics Cards */}
|
||||
<Grid gutter="md">
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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 }}>
|
||||
<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"}>
|
||||
Alert Stunting
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c="red">
|
||||
{healthStats.alertStunting}
|
||||
</Text>
|
||||
</Stack>
|
||||
<ThemeIcon variant="light" color="red" size="xl" radius="xl">
|
||||
<IconStethoscope size={24} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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"}>
|
||||
Balita Terdaftar
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{healthStats.balita}
|
||||
</Text>
|
||||
</Stack>
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
color="darmasaba-success"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
>
|
||||
<IconBabyCarriage size={24} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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"}>
|
||||
Posyandu Aktif
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{healthStats.posyanduAktif}
|
||||
</Text>
|
||||
</Stack>
|
||||
<ThemeIcon variant="light" color="darmasaba-warning" size="xl" radius="xl">
|
||||
<IconMedicalCross size={24} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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"}>
|
||||
Alert Stunting
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c="red">
|
||||
{healthStats.alertStunting}
|
||||
</Text>
|
||||
</Stack>
|
||||
<ThemeIcon variant="light" color="red" size="xl" radius="xl">
|
||||
<IconStethoscope size={24} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
|
||||
{/* Health Progress Bars */}
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Statistik Kesehatan</Title>
|
||||
<Stack gap="md">
|
||||
{healthProgress.map((item, index) => (
|
||||
<div key={index}>
|
||||
<Group justify="space-between" mb={5}>
|
||||
<Text size="sm" fw={500} c={dark ? "dark.0" : "black"}>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text size="sm" fw={600} c={dark ? "dark.0" : "black"}>
|
||||
{item.value}%
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={item.value}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
color={item.color}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
<GridCol span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<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"}>
|
||||
Posyandu Aktif
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{healthStats.posyanduAktif}
|
||||
</Text>
|
||||
</Stack>
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
color="darmasaba-warning"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
>
|
||||
<IconMedicalCross size={24} />
|
||||
</ThemeIcon>
|
||||
</Group>
|
||||
</Card>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
<Grid gutter="md">
|
||||
{/* Jadwal Posyandu */}
|
||||
<GridCol span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }}>
|
||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>Jadwal Posyandu</Title>
|
||||
<Stack gap="sm">
|
||||
{posyanduSchedule.map((item, index) => (
|
||||
<Card key={index} p="md" radius="md" withBorder bg={dark ? "#263852ff" : "#F1F5F9"} style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }} h="100%">
|
||||
<Group justify="space-between">
|
||||
<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>
|
||||
</Stack>
|
||||
<Badge variant="light" color="darmasaba-blue">
|
||||
{item.jam}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
</GridCol>
|
||||
{/* Health Progress Bars */}
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
||||
Statistik Kesehatan
|
||||
</Title>
|
||||
<Stack gap="md">
|
||||
{healthProgress.map((item, index) => (
|
||||
<div key={index}>
|
||||
<Group justify="space-between" mb={5}>
|
||||
<Text size="sm" fw={500} c={dark ? "dark.0" : "black"}>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text size="sm" fw={600} c={dark ? "dark.0" : "black"}>
|
||||
{item.value}%
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={item.value}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
color={item.color}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Pendidikan */}
|
||||
<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"}>Pendidikan</Title>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>TK / PAUD</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.tk}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>SD</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.sd}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>SMP</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.smp}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>SMA</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.siswa.sma}</Text>
|
||||
</Group>
|
||||
|
||||
<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>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.sekolah.jumlah}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between" mt="sm">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>Jumlah Tenaga Pengajar</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>{educationStats.sekolah.guru}</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Card>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
<Grid gutter="md">
|
||||
{/* Jadwal Posyandu */}
|
||||
<GridCol span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
>
|
||||
<Title order={3} mb="md" c={dark ? "dark.0" : "black"}>
|
||||
Jadwal Posyandu
|
||||
</Title>
|
||||
<Stack gap="sm">
|
||||
{posyanduSchedule.map((item, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#263852ff" : "#F1F5F9"}
|
||||
style={{ borderColor: dark ? "#263852ff" : "#F1F5F9" }}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<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>
|
||||
</Stack>
|
||||
<Badge variant="light" color="darmasaba-blue">
|
||||
{item.jam}
|
||||
</Badge>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
</GridCol>
|
||||
|
||||
<Grid gutter="md">
|
||||
{/* Beasiswa Desa */}
|
||||
<GridCol span={{ base: 12, lg: 6 }}>
|
||||
<Card p="md" radius="md" withBorder bg={dark ? "#141D34" : "white"} style={{ borderColor: dark ? "#141D34" : "white" }} h="100%">
|
||||
<Group justify="space-between" align="center">
|
||||
<Stack gap={0}>
|
||||
<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>
|
||||
<ThemeIcon variant="light" color="darmasaba-success" size="xl" radius="xl">
|
||||
<IconAward size={24} />
|
||||
</ThemeIcon>
|
||||
</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>
|
||||
</Card>
|
||||
</GridCol>
|
||||
{/* Pendidikan */}
|
||||
<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"}>
|
||||
Pendidikan
|
||||
</Title>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
TK / PAUD
|
||||
</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{educationStats.siswa.tk}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
SD
|
||||
</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{educationStats.siswa.sd}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
SMP
|
||||
</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{educationStats.siswa.smp}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
SMA
|
||||
</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{educationStats.siswa.sma}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{/* Kalender Event Budaya */}
|
||||
<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"}>Kalender Event Budaya</Title>
|
||||
<List spacing="sm">
|
||||
{culturalEvents.map((event, index) => (
|
||||
<List.Item key={index} icon={
|
||||
<ThemeIcon color="darmasaba-blue" size={24} radius="xl">
|
||||
<IconCalendarEvent size={12} />
|
||||
</ThemeIcon>
|
||||
}>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>{event.nama}</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{event.lokasi}</Text>
|
||||
</Group>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>{event.tanggal}</Text>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
<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>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{educationStats.sekolah.jumlah}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between" mt="sm">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
Jumlah Tenaga Pengajar
|
||||
</Text>
|
||||
<Text fw={700} c={dark ? "dark.0" : "black"}>
|
||||
{educationStats.sekolah.guru}
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Card>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
|
||||
<Grid gutter="md">
|
||||
{/* Beasiswa Desa */}
|
||||
<GridCol span={{ base: 12, lg: 6 }}>
|
||||
<Card
|
||||
p="md"
|
||||
radius="md"
|
||||
withBorder
|
||||
bg={dark ? "#141D34" : "white"}
|
||||
style={{ borderColor: dark ? "#141D34" : "white" }}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" align="center">
|
||||
<Stack gap={0}>
|
||||
<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>
|
||||
<ThemeIcon
|
||||
variant="light"
|
||||
color="darmasaba-success"
|
||||
size="xl"
|
||||
radius="xl"
|
||||
>
|
||||
<IconAward size={24} />
|
||||
</ThemeIcon>
|
||||
</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>
|
||||
</Card>
|
||||
</GridCol>
|
||||
|
||||
{/* Kalender Event Budaya */}
|
||||
<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"}>
|
||||
Kalender Event Budaya
|
||||
</Title>
|
||||
<List spacing="sm">
|
||||
{culturalEvents.map((event, index) => (
|
||||
<List.Item
|
||||
key={index}
|
||||
icon={
|
||||
<ThemeIcon color="darmasaba-blue" size={24} radius="xl">
|
||||
<IconCalendarEvent size={12} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
<Group justify="space-between">
|
||||
<Text fw={500} c={dark ? "dark.0" : "black"}>
|
||||
{event.nama}
|
||||
</Text>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{event.lokasi}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" c={dark ? "dark.3" : "dimmed"}>
|
||||
{event.tanggal}
|
||||
</Text>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SosialPage;
|
||||
export default SosialPage;
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
Box,
|
||||
Card as MantineCard,
|
||||
type CardProps as MantineCardProps,
|
||||
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type React from "react";
|
||||
|
||||
@@ -1,90 +1,86 @@
|
||||
import { Card, useMantineTheme, useComputedColorScheme } from '@mantine/core';
|
||||
import type { CardProps } from '@mantine/core';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { CardProps } from "@mantine/core";
|
||||
import { Card, useComputedColorScheme, useMantineTheme } from "@mantine/core";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface HelpCardProps extends CardProps {
|
||||
children: ReactNode;
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
minHeight?: string | number; // Allow specifying a minimum height
|
||||
children: ReactNode;
|
||||
icon?: ReactNode;
|
||||
title?: string;
|
||||
minHeight?: string | number; // Allow specifying a minimum height
|
||||
}
|
||||
|
||||
export const HelpCard = ({
|
||||
children,
|
||||
icon,
|
||||
title,
|
||||
minHeight = 'auto', // Default to auto, but allow override
|
||||
...props
|
||||
children,
|
||||
icon,
|
||||
title,
|
||||
minHeight = "auto", // Default to auto, but allow override
|
||||
...props
|
||||
}: HelpCardProps) => {
|
||||
const theme = useMantineTheme();
|
||||
const colorScheme = useComputedColorScheme('light');
|
||||
const isDark = colorScheme === 'dark';
|
||||
const theme = useMantineTheme();
|
||||
const colorScheme = useComputedColorScheme("light");
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="xl"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={{
|
||||
backgroundColor: isDark ? theme.colors.dark[7] : theme.white,
|
||||
borderRadius: '16px',
|
||||
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
|
||||
border: `1px solid ${
|
||||
isDark ? theme.colors.dark[4] : theme.colors.gray[3]
|
||||
}`,
|
||||
minHeight, // Apply the minimum height
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{(icon || title) && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{icon && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: isDark
|
||||
? theme.colors.blue[8]
|
||||
: theme.colors.blue[0],
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<Card
|
||||
shadow="sm"
|
||||
padding="xl"
|
||||
radius="md"
|
||||
withBorder
|
||||
style={{
|
||||
backgroundColor: isDark ? theme.colors.dark[7] : theme.white,
|
||||
borderRadius: "16px",
|
||||
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
||||
border: `1px solid ${
|
||||
isDark ? theme.colors.dark[4] : theme.colors.gray[3]
|
||||
}`,
|
||||
minHeight, // Apply the minimum height
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{(icon || title) && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
>
|
||||
{icon && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: isDark
|
||||
? theme.colors.blue[8]
|
||||
: theme.colors.blue[0],
|
||||
borderRadius: "8px",
|
||||
padding: "8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{title && (
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: '16px',
|
||||
fontWeight: 600,
|
||||
color: isDark
|
||||
? theme.colors.dark[0]
|
||||
: theme.colors.dark[9],
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{title && (
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
color: isDark ? theme.colors.dark[0] : theme.colors.dark[9],
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ flex: 1 }}>
|
||||
{children}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
<div style={{ flex: 1 }}>{children}</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
BIN
src/components/ui/logo-desa-plus.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -14,10 +14,9 @@ import { Inspector } from "react-dev-inspector";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import "./index.css";
|
||||
import '@mantine/charts/styles.css';
|
||||
import "@mantine/charts/styles.css";
|
||||
import { IS_DEV, VITE_PUBLIC_URL } from "./utils/env";
|
||||
|
||||
|
||||
// Create a new router instance
|
||||
export const router = createRouter({
|
||||
routeTree,
|
||||
@@ -102,8 +101,6 @@ const theme = createTheme({
|
||||
primaryColor: "darmasaba-blue",
|
||||
});
|
||||
|
||||
|
||||
|
||||
const InspectorWrapper = IS_DEV
|
||||
? Inspector
|
||||
: ({ children }: { children: React.ReactNode }) => <>{children}</>;
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
|
||||
50
src/index.ts
@@ -6,8 +6,22 @@ import { Elysia } from "elysia";
|
||||
import api from "./api";
|
||||
import { openInEditor } from "./utils/open-in-editor";
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
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);
|
||||
|
||||
if (!isProduction) {
|
||||
@@ -81,10 +95,25 @@ if (!isProduction) {
|
||||
getHeader(name: string) {
|
||||
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>,
|
||||
end(data: any) {
|
||||
// Handle potential Buffer or string data from Vite
|
||||
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) {
|
||||
body = data;
|
||||
} else if (typeof data === "string") {
|
||||
@@ -144,6 +173,11 @@ if (!isProduction) {
|
||||
if (fs.existsSync(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
|
||||
@@ -159,8 +193,18 @@ if (!isProduction) {
|
||||
) {
|
||||
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
|
||||
else if (pathname.includes("assetlinks.json")) {
|
||||
if (pathname.includes("assetlinks.json")) {
|
||||
const srcFilename = pathname.includes("assetlinks.json")
|
||||
? ".well-known/assetlinks.json"
|
||||
: filename;
|
||||
@@ -198,10 +242,10 @@ if (!isProduction) {
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(3000);
|
||||
app.listen(PORT);
|
||||
|
||||
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;
|
||||
|
||||
@@ -60,16 +60,39 @@ type RouteRule = {
|
||||
};
|
||||
|
||||
const routeRules: RouteRule[] = [
|
||||
// Public routes - no auth required
|
||||
{
|
||||
match: (p) => p === "/" || p === "/signin" || p === "/signup",
|
||||
requireAuth: false,
|
||||
},
|
||||
// Profile routes - auth required for all roles
|
||||
{
|
||||
match: (p) => p === "/profile" || p.startsWith("/profile/"),
|
||||
requireAuth: true,
|
||||
redirectTo: "/signin",
|
||||
},
|
||||
// Dashboard and main pages - auth required for all roles (not just admin)
|
||||
{
|
||||
match: (p) => p === "/dashboard" || p.startsWith("/dashboard/"),
|
||||
match: (p) =>
|
||||
p.startsWith("/kinerja-divisi") ||
|
||||
p.startsWith("/pengaduan") ||
|
||||
p.startsWith("/jenna") ||
|
||||
p.startsWith("/demografi") ||
|
||||
p.startsWith("/keuangan") ||
|
||||
p.startsWith("/bumdes") ||
|
||||
p.startsWith("/sosial") ||
|
||||
p.startsWith("/keamanan") ||
|
||||
p.startsWith("/bantuan") ||
|
||||
p.startsWith("/pengaturan"),
|
||||
requireAuth: true,
|
||||
redirectTo: "/signin",
|
||||
},
|
||||
// Admin routes - auth required with admin role only
|
||||
{
|
||||
match: (p) => p.startsWith("/admin"),
|
||||
requireAuth: true,
|
||||
requiredRole: "admin",
|
||||
redirectTo: "/profile",
|
||||
redirectTo: "/signin",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -98,15 +121,22 @@ export function createProtectedRoute(options: ProtectedRouteOptions = {}) {
|
||||
location: { pathname: string; href: string };
|
||||
}) => {
|
||||
const rule = findRouteRule(location.pathname);
|
||||
|
||||
// If no rule matches, allow access by default
|
||||
if (!rule) return;
|
||||
|
||||
// If route explicitly doesn't require auth, allow access
|
||||
if (rule.requireAuth === false) return;
|
||||
|
||||
const session = await fetchSession();
|
||||
const user = session?.user;
|
||||
|
||||
// If auth is required but user is not logged in, redirect to login
|
||||
if (rule.requireAuth && !user) {
|
||||
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
||||
}
|
||||
|
||||
// If specific role is required, check it
|
||||
if (rule.requiredRole && user?.role !== rule.requiredRole) {
|
||||
redirectToLogin(rule.redirectTo ?? redirectTo, location.href);
|
||||
}
|
||||
|
||||
@@ -9,35 +9,38 @@
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as SosialRouteImport } from './routes/sosial'
|
||||
import { Route as SignupRouteImport } from './routes/signup'
|
||||
import { Route as SigninRouteImport } from './routes/signin'
|
||||
import { Route as DashboardRouteRouteImport } from './routes/dashboard/route'
|
||||
import { Route as PengaduanLayananPublikRouteImport } from './routes/pengaduan-layanan-publik'
|
||||
import { Route as KinerjaDivisiRouteImport } from './routes/kinerja-divisi'
|
||||
import { Route as KeuanganAnggaranRouteImport } from './routes/keuangan-anggaran'
|
||||
import { Route as KeamananRouteImport } from './routes/keamanan'
|
||||
import { Route as JennaAnalyticRouteImport } from './routes/jenna-analytic'
|
||||
import { Route as DemografiPekerjaanRouteImport } from './routes/demografi-pekerjaan'
|
||||
import { Route as BumdesRouteImport } from './routes/bumdes'
|
||||
import { Route as BantuanRouteImport } from './routes/bantuan'
|
||||
import { Route as PengaturanRouteRouteImport } from './routes/pengaturan/route'
|
||||
import { Route as AdminRouteRouteImport } from './routes/admin/route'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as UsersIndexRouteImport } from './routes/users/index'
|
||||
import { Route as ProfileIndexRouteImport } from './routes/profile/index'
|
||||
import { Route as DashboardIndexRouteImport } from './routes/dashboard/index'
|
||||
import { Route as AdminIndexRouteImport } from './routes/admin/index'
|
||||
import { Route as UsersIdRouteImport } from './routes/users/$id'
|
||||
import { Route as ProfileEditRouteImport } from './routes/profile/edit'
|
||||
import { Route as DashboardSosialRouteImport } from './routes/dashboard/sosial'
|
||||
import { Route as DashboardPengaduanLayananPublikRouteImport } from './routes/dashboard/pengaduan-layanan-publik'
|
||||
import { Route as DashboardKinerjaDivisiRouteImport } from './routes/dashboard/kinerja-divisi'
|
||||
import { Route as DashboardKeuanganAnggaranRouteImport } from './routes/dashboard/keuangan-anggaran'
|
||||
import { Route as DashboardKeamananRouteImport } from './routes/dashboard/keamanan'
|
||||
import { Route as DashboardJennaAnalyticRouteImport } from './routes/dashboard/jenna-analytic'
|
||||
import { Route as DashboardDemografiPekerjaanRouteImport } from './routes/dashboard/demografi-pekerjaan'
|
||||
import { Route as DashboardBumdesRouteImport } from './routes/dashboard/bumdes'
|
||||
import { Route as DashboardBantuanRouteImport } from './routes/dashboard/bantuan'
|
||||
import { Route as PengaturanUmumRouteImport } from './routes/pengaturan/umum'
|
||||
import { Route as PengaturanNotifikasiRouteImport } from './routes/pengaturan/notifikasi'
|
||||
import { Route as PengaturanKeamananRouteImport } from './routes/pengaturan/keamanan'
|
||||
import { Route as PengaturanAksesDanTimRouteImport } from './routes/pengaturan/akses-dan-tim'
|
||||
import { Route as AdminUsersRouteImport } from './routes/admin/users'
|
||||
import { Route as AdminSettingsRouteImport } from './routes/admin/settings'
|
||||
import { Route as AdminApikeyRouteImport } from './routes/admin/apikey'
|
||||
import { Route as DashboardPengaturanRouteRouteImport } from './routes/dashboard/pengaturan/route'
|
||||
import { Route as DashboardPengaturanUmumRouteImport } from './routes/dashboard/pengaturan/umum'
|
||||
import { Route as DashboardPengaturanNotifikasiRouteImport } from './routes/dashboard/pengaturan/notifikasi'
|
||||
import { Route as DashboardPengaturanKeamananRouteImport } from './routes/dashboard/pengaturan/keamanan'
|
||||
import { Route as DashboardPengaturanAksesDanTimRouteImport } from './routes/dashboard/pengaturan/akses-dan-tim'
|
||||
|
||||
const SosialRoute = SosialRouteImport.update({
|
||||
id: '/sosial',
|
||||
path: '/sosial',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const SignupRoute = SignupRouteImport.update({
|
||||
id: '/signup',
|
||||
path: '/signup',
|
||||
@@ -48,9 +51,49 @@ const SigninRoute = SigninRouteImport.update({
|
||||
path: '/signin',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DashboardRouteRoute = DashboardRouteRouteImport.update({
|
||||
id: '/dashboard',
|
||||
path: '/dashboard',
|
||||
const PengaduanLayananPublikRoute = PengaduanLayananPublikRouteImport.update({
|
||||
id: '/pengaduan-layanan-publik',
|
||||
path: '/pengaduan-layanan-publik',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const KinerjaDivisiRoute = KinerjaDivisiRouteImport.update({
|
||||
id: '/kinerja-divisi',
|
||||
path: '/kinerja-divisi',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const KeuanganAnggaranRoute = KeuanganAnggaranRouteImport.update({
|
||||
id: '/keuangan-anggaran',
|
||||
path: '/keuangan-anggaran',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const KeamananRoute = KeamananRouteImport.update({
|
||||
id: '/keamanan',
|
||||
path: '/keamanan',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const JennaAnalyticRoute = JennaAnalyticRouteImport.update({
|
||||
id: '/jenna-analytic',
|
||||
path: '/jenna-analytic',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DemografiPekerjaanRoute = DemografiPekerjaanRouteImport.update({
|
||||
id: '/demografi-pekerjaan',
|
||||
path: '/demografi-pekerjaan',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const BumdesRoute = BumdesRouteImport.update({
|
||||
id: '/bumdes',
|
||||
path: '/bumdes',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const BantuanRoute = BantuanRouteImport.update({
|
||||
id: '/bantuan',
|
||||
path: '/bantuan',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const PengaturanRouteRoute = PengaturanRouteRouteImport.update({
|
||||
id: '/pengaturan',
|
||||
path: '/pengaturan',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminRouteRoute = AdminRouteRouteImport.update({
|
||||
@@ -73,11 +116,6 @@ const ProfileIndexRoute = ProfileIndexRouteImport.update({
|
||||
path: '/profile/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DashboardIndexRoute = DashboardIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
} as any)
|
||||
const AdminIndexRoute = AdminIndexRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
@@ -93,53 +131,25 @@ const ProfileEditRoute = ProfileEditRouteImport.update({
|
||||
path: '/profile/edit',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const DashboardSosialRoute = DashboardSosialRouteImport.update({
|
||||
id: '/sosial',
|
||||
path: '/sosial',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
const PengaturanUmumRoute = PengaturanUmumRouteImport.update({
|
||||
id: '/umum',
|
||||
path: '/umum',
|
||||
getParentRoute: () => PengaturanRouteRoute,
|
||||
} as any)
|
||||
const DashboardPengaduanLayananPublikRoute =
|
||||
DashboardPengaduanLayananPublikRouteImport.update({
|
||||
id: '/pengaduan-layanan-publik',
|
||||
path: '/pengaduan-layanan-publik',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
} as any)
|
||||
const DashboardKinerjaDivisiRoute = DashboardKinerjaDivisiRouteImport.update({
|
||||
id: '/kinerja-divisi',
|
||||
path: '/kinerja-divisi',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
const PengaturanNotifikasiRoute = PengaturanNotifikasiRouteImport.update({
|
||||
id: '/notifikasi',
|
||||
path: '/notifikasi',
|
||||
getParentRoute: () => PengaturanRouteRoute,
|
||||
} as any)
|
||||
const DashboardKeuanganAnggaranRoute =
|
||||
DashboardKeuanganAnggaranRouteImport.update({
|
||||
id: '/keuangan-anggaran',
|
||||
path: '/keuangan-anggaran',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
} as any)
|
||||
const DashboardKeamananRoute = DashboardKeamananRouteImport.update({
|
||||
const PengaturanKeamananRoute = PengaturanKeamananRouteImport.update({
|
||||
id: '/keamanan',
|
||||
path: '/keamanan',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
getParentRoute: () => PengaturanRouteRoute,
|
||||
} as any)
|
||||
const DashboardJennaAnalyticRoute = DashboardJennaAnalyticRouteImport.update({
|
||||
id: '/jenna-analytic',
|
||||
path: '/jenna-analytic',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
} as any)
|
||||
const DashboardDemografiPekerjaanRoute =
|
||||
DashboardDemografiPekerjaanRouteImport.update({
|
||||
id: '/demografi-pekerjaan',
|
||||
path: '/demografi-pekerjaan',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
} as any)
|
||||
const DashboardBumdesRoute = DashboardBumdesRouteImport.update({
|
||||
id: '/bumdes',
|
||||
path: '/bumdes',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
} as any)
|
||||
const DashboardBantuanRoute = DashboardBantuanRouteImport.update({
|
||||
id: '/bantuan',
|
||||
path: '/bantuan',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
const PengaturanAksesDanTimRoute = PengaturanAksesDanTimRouteImport.update({
|
||||
id: '/akses-dan-tim',
|
||||
path: '/akses-dan-tim',
|
||||
getParentRoute: () => PengaturanRouteRoute,
|
||||
} as any)
|
||||
const AdminUsersRoute = AdminUsersRouteImport.update({
|
||||
id: '/users',
|
||||
@@ -156,222 +166,192 @@ const AdminApikeyRoute = AdminApikeyRouteImport.update({
|
||||
path: '/apikey',
|
||||
getParentRoute: () => AdminRouteRoute,
|
||||
} as any)
|
||||
const DashboardPengaturanRouteRoute =
|
||||
DashboardPengaturanRouteRouteImport.update({
|
||||
id: '/pengaturan',
|
||||
path: '/pengaturan',
|
||||
getParentRoute: () => DashboardRouteRoute,
|
||||
} as any)
|
||||
const DashboardPengaturanUmumRoute = DashboardPengaturanUmumRouteImport.update({
|
||||
id: '/umum',
|
||||
path: '/umum',
|
||||
getParentRoute: () => DashboardPengaturanRouteRoute,
|
||||
} as any)
|
||||
const DashboardPengaturanNotifikasiRoute =
|
||||
DashboardPengaturanNotifikasiRouteImport.update({
|
||||
id: '/notifikasi',
|
||||
path: '/notifikasi',
|
||||
getParentRoute: () => DashboardPengaturanRouteRoute,
|
||||
} as any)
|
||||
const DashboardPengaturanKeamananRoute =
|
||||
DashboardPengaturanKeamananRouteImport.update({
|
||||
id: '/keamanan',
|
||||
path: '/keamanan',
|
||||
getParentRoute: () => DashboardPengaturanRouteRoute,
|
||||
} as any)
|
||||
const DashboardPengaturanAksesDanTimRoute =
|
||||
DashboardPengaturanAksesDanTimRouteImport.update({
|
||||
id: '/akses-dan-tim',
|
||||
path: '/akses-dan-tim',
|
||||
getParentRoute: () => DashboardPengaturanRouteRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/admin': typeof AdminRouteRouteWithChildren
|
||||
'/dashboard': typeof DashboardRouteRouteWithChildren
|
||||
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
||||
'/bantuan': typeof BantuanRoute
|
||||
'/bumdes': typeof BumdesRoute
|
||||
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
||||
'/jenna-analytic': typeof JennaAnalyticRoute
|
||||
'/keamanan': typeof KeamananRoute
|
||||
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
||||
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
||||
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
||||
'/signin': typeof SigninRoute
|
||||
'/signup': typeof SignupRoute
|
||||
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
||||
'/sosial': typeof SosialRoute
|
||||
'/admin/apikey': typeof AdminApikeyRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
||||
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
||||
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
||||
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
||||
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
||||
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
||||
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
||||
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
||||
'/dashboard/sosial': typeof DashboardSosialRoute
|
||||
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
||||
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
||||
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
||||
'/pengaturan/umum': typeof PengaturanUmumRoute
|
||||
'/profile/edit': typeof ProfileEditRoute
|
||||
'/users/$id': typeof UsersIdRoute
|
||||
'/admin/': typeof AdminIndexRoute
|
||||
'/dashboard/': typeof DashboardIndexRoute
|
||||
'/profile/': typeof ProfileIndexRoute
|
||||
'/users/': typeof UsersIndexRoute
|
||||
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
||||
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
||||
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
||||
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
||||
'/bantuan': typeof BantuanRoute
|
||||
'/bumdes': typeof BumdesRoute
|
||||
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
||||
'/jenna-analytic': typeof JennaAnalyticRoute
|
||||
'/keamanan': typeof KeamananRoute
|
||||
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
||||
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
||||
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
||||
'/signin': typeof SigninRoute
|
||||
'/signup': typeof SignupRoute
|
||||
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
||||
'/sosial': typeof SosialRoute
|
||||
'/admin/apikey': typeof AdminApikeyRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
||||
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
||||
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
||||
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
||||
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
||||
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
||||
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
||||
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
||||
'/dashboard/sosial': typeof DashboardSosialRoute
|
||||
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
||||
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
||||
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
||||
'/pengaturan/umum': typeof PengaturanUmumRoute
|
||||
'/profile/edit': typeof ProfileEditRoute
|
||||
'/users/$id': typeof UsersIdRoute
|
||||
'/admin': typeof AdminIndexRoute
|
||||
'/dashboard': typeof DashboardIndexRoute
|
||||
'/profile': typeof ProfileIndexRoute
|
||||
'/users': typeof UsersIndexRoute
|
||||
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
||||
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
||||
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
||||
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/admin': typeof AdminRouteRouteWithChildren
|
||||
'/dashboard': typeof DashboardRouteRouteWithChildren
|
||||
'/pengaturan': typeof PengaturanRouteRouteWithChildren
|
||||
'/bantuan': typeof BantuanRoute
|
||||
'/bumdes': typeof BumdesRoute
|
||||
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute
|
||||
'/jenna-analytic': typeof JennaAnalyticRoute
|
||||
'/keamanan': typeof KeamananRoute
|
||||
'/keuangan-anggaran': typeof KeuanganAnggaranRoute
|
||||
'/kinerja-divisi': typeof KinerjaDivisiRoute
|
||||
'/pengaduan-layanan-publik': typeof PengaduanLayananPublikRoute
|
||||
'/signin': typeof SigninRoute
|
||||
'/signup': typeof SignupRoute
|
||||
'/dashboard/pengaturan': typeof DashboardPengaturanRouteRouteWithChildren
|
||||
'/sosial': typeof SosialRoute
|
||||
'/admin/apikey': typeof AdminApikeyRoute
|
||||
'/admin/settings': typeof AdminSettingsRoute
|
||||
'/admin/users': typeof AdminUsersRoute
|
||||
'/dashboard/bantuan': typeof DashboardBantuanRoute
|
||||
'/dashboard/bumdes': typeof DashboardBumdesRoute
|
||||
'/dashboard/demografi-pekerjaan': typeof DashboardDemografiPekerjaanRoute
|
||||
'/dashboard/jenna-analytic': typeof DashboardJennaAnalyticRoute
|
||||
'/dashboard/keamanan': typeof DashboardKeamananRoute
|
||||
'/dashboard/keuangan-anggaran': typeof DashboardKeuanganAnggaranRoute
|
||||
'/dashboard/kinerja-divisi': typeof DashboardKinerjaDivisiRoute
|
||||
'/dashboard/pengaduan-layanan-publik': typeof DashboardPengaduanLayananPublikRoute
|
||||
'/dashboard/sosial': typeof DashboardSosialRoute
|
||||
'/pengaturan/akses-dan-tim': typeof PengaturanAksesDanTimRoute
|
||||
'/pengaturan/keamanan': typeof PengaturanKeamananRoute
|
||||
'/pengaturan/notifikasi': typeof PengaturanNotifikasiRoute
|
||||
'/pengaturan/umum': typeof PengaturanUmumRoute
|
||||
'/profile/edit': typeof ProfileEditRoute
|
||||
'/users/$id': typeof UsersIdRoute
|
||||
'/admin/': typeof AdminIndexRoute
|
||||
'/dashboard/': typeof DashboardIndexRoute
|
||||
'/profile/': typeof ProfileIndexRoute
|
||||
'/users/': typeof UsersIndexRoute
|
||||
'/dashboard/pengaturan/akses-dan-tim': typeof DashboardPengaturanAksesDanTimRoute
|
||||
'/dashboard/pengaturan/keamanan': typeof DashboardPengaturanKeamananRoute
|
||||
'/dashboard/pengaturan/notifikasi': typeof DashboardPengaturanNotifikasiRoute
|
||||
'/dashboard/pengaturan/umum': typeof DashboardPengaturanUmumRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/admin'
|
||||
| '/dashboard'
|
||||
| '/pengaturan'
|
||||
| '/bantuan'
|
||||
| '/bumdes'
|
||||
| '/demografi-pekerjaan'
|
||||
| '/jenna-analytic'
|
||||
| '/keamanan'
|
||||
| '/keuangan-anggaran'
|
||||
| '/kinerja-divisi'
|
||||
| '/pengaduan-layanan-publik'
|
||||
| '/signin'
|
||||
| '/signup'
|
||||
| '/dashboard/pengaturan'
|
||||
| '/sosial'
|
||||
| '/admin/apikey'
|
||||
| '/admin/settings'
|
||||
| '/admin/users'
|
||||
| '/dashboard/bantuan'
|
||||
| '/dashboard/bumdes'
|
||||
| '/dashboard/demografi-pekerjaan'
|
||||
| '/dashboard/jenna-analytic'
|
||||
| '/dashboard/keamanan'
|
||||
| '/dashboard/keuangan-anggaran'
|
||||
| '/dashboard/kinerja-divisi'
|
||||
| '/dashboard/pengaduan-layanan-publik'
|
||||
| '/dashboard/sosial'
|
||||
| '/pengaturan/akses-dan-tim'
|
||||
| '/pengaturan/keamanan'
|
||||
| '/pengaturan/notifikasi'
|
||||
| '/pengaturan/umum'
|
||||
| '/profile/edit'
|
||||
| '/users/$id'
|
||||
| '/admin/'
|
||||
| '/dashboard/'
|
||||
| '/profile/'
|
||||
| '/users/'
|
||||
| '/dashboard/pengaturan/akses-dan-tim'
|
||||
| '/dashboard/pengaturan/keamanan'
|
||||
| '/dashboard/pengaturan/notifikasi'
|
||||
| '/dashboard/pengaturan/umum'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/pengaturan'
|
||||
| '/bantuan'
|
||||
| '/bumdes'
|
||||
| '/demografi-pekerjaan'
|
||||
| '/jenna-analytic'
|
||||
| '/keamanan'
|
||||
| '/keuangan-anggaran'
|
||||
| '/kinerja-divisi'
|
||||
| '/pengaduan-layanan-publik'
|
||||
| '/signin'
|
||||
| '/signup'
|
||||
| '/dashboard/pengaturan'
|
||||
| '/sosial'
|
||||
| '/admin/apikey'
|
||||
| '/admin/settings'
|
||||
| '/admin/users'
|
||||
| '/dashboard/bantuan'
|
||||
| '/dashboard/bumdes'
|
||||
| '/dashboard/demografi-pekerjaan'
|
||||
| '/dashboard/jenna-analytic'
|
||||
| '/dashboard/keamanan'
|
||||
| '/dashboard/keuangan-anggaran'
|
||||
| '/dashboard/kinerja-divisi'
|
||||
| '/dashboard/pengaduan-layanan-publik'
|
||||
| '/dashboard/sosial'
|
||||
| '/pengaturan/akses-dan-tim'
|
||||
| '/pengaturan/keamanan'
|
||||
| '/pengaturan/notifikasi'
|
||||
| '/pengaturan/umum'
|
||||
| '/profile/edit'
|
||||
| '/users/$id'
|
||||
| '/admin'
|
||||
| '/dashboard'
|
||||
| '/profile'
|
||||
| '/users'
|
||||
| '/dashboard/pengaturan/akses-dan-tim'
|
||||
| '/dashboard/pengaturan/keamanan'
|
||||
| '/dashboard/pengaturan/notifikasi'
|
||||
| '/dashboard/pengaturan/umum'
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/admin'
|
||||
| '/dashboard'
|
||||
| '/pengaturan'
|
||||
| '/bantuan'
|
||||
| '/bumdes'
|
||||
| '/demografi-pekerjaan'
|
||||
| '/jenna-analytic'
|
||||
| '/keamanan'
|
||||
| '/keuangan-anggaran'
|
||||
| '/kinerja-divisi'
|
||||
| '/pengaduan-layanan-publik'
|
||||
| '/signin'
|
||||
| '/signup'
|
||||
| '/dashboard/pengaturan'
|
||||
| '/sosial'
|
||||
| '/admin/apikey'
|
||||
| '/admin/settings'
|
||||
| '/admin/users'
|
||||
| '/dashboard/bantuan'
|
||||
| '/dashboard/bumdes'
|
||||
| '/dashboard/demografi-pekerjaan'
|
||||
| '/dashboard/jenna-analytic'
|
||||
| '/dashboard/keamanan'
|
||||
| '/dashboard/keuangan-anggaran'
|
||||
| '/dashboard/kinerja-divisi'
|
||||
| '/dashboard/pengaduan-layanan-publik'
|
||||
| '/dashboard/sosial'
|
||||
| '/pengaturan/akses-dan-tim'
|
||||
| '/pengaturan/keamanan'
|
||||
| '/pengaturan/notifikasi'
|
||||
| '/pengaturan/umum'
|
||||
| '/profile/edit'
|
||||
| '/users/$id'
|
||||
| '/admin/'
|
||||
| '/dashboard/'
|
||||
| '/profile/'
|
||||
| '/users/'
|
||||
| '/dashboard/pengaturan/akses-dan-tim'
|
||||
| '/dashboard/pengaturan/keamanan'
|
||||
| '/dashboard/pengaturan/notifikasi'
|
||||
| '/dashboard/pengaturan/umum'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AdminRouteRoute: typeof AdminRouteRouteWithChildren
|
||||
DashboardRouteRoute: typeof DashboardRouteRouteWithChildren
|
||||
PengaturanRouteRoute: typeof PengaturanRouteRouteWithChildren
|
||||
BantuanRoute: typeof BantuanRoute
|
||||
BumdesRoute: typeof BumdesRoute
|
||||
DemografiPekerjaanRoute: typeof DemografiPekerjaanRoute
|
||||
JennaAnalyticRoute: typeof JennaAnalyticRoute
|
||||
KeamananRoute: typeof KeamananRoute
|
||||
KeuanganAnggaranRoute: typeof KeuanganAnggaranRoute
|
||||
KinerjaDivisiRoute: typeof KinerjaDivisiRoute
|
||||
PengaduanLayananPublikRoute: typeof PengaduanLayananPublikRoute
|
||||
SigninRoute: typeof SigninRoute
|
||||
SignupRoute: typeof SignupRoute
|
||||
SosialRoute: typeof SosialRoute
|
||||
ProfileEditRoute: typeof ProfileEditRoute
|
||||
UsersIdRoute: typeof UsersIdRoute
|
||||
ProfileIndexRoute: typeof ProfileIndexRoute
|
||||
@@ -380,6 +360,13 @@ export interface RootRouteChildren {
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/sosial': {
|
||||
id: '/sosial'
|
||||
path: '/sosial'
|
||||
fullPath: '/sosial'
|
||||
preLoaderRoute: typeof SosialRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/signup': {
|
||||
id: '/signup'
|
||||
path: '/signup'
|
||||
@@ -394,11 +381,67 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof SigninRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/dashboard': {
|
||||
id: '/dashboard'
|
||||
path: '/dashboard'
|
||||
fullPath: '/dashboard'
|
||||
preLoaderRoute: typeof DashboardRouteRouteImport
|
||||
'/pengaduan-layanan-publik': {
|
||||
id: '/pengaduan-layanan-publik'
|
||||
path: '/pengaduan-layanan-publik'
|
||||
fullPath: '/pengaduan-layanan-publik'
|
||||
preLoaderRoute: typeof PengaduanLayananPublikRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/kinerja-divisi': {
|
||||
id: '/kinerja-divisi'
|
||||
path: '/kinerja-divisi'
|
||||
fullPath: '/kinerja-divisi'
|
||||
preLoaderRoute: typeof KinerjaDivisiRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/keuangan-anggaran': {
|
||||
id: '/keuangan-anggaran'
|
||||
path: '/keuangan-anggaran'
|
||||
fullPath: '/keuangan-anggaran'
|
||||
preLoaderRoute: typeof KeuanganAnggaranRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/keamanan': {
|
||||
id: '/keamanan'
|
||||
path: '/keamanan'
|
||||
fullPath: '/keamanan'
|
||||
preLoaderRoute: typeof KeamananRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/jenna-analytic': {
|
||||
id: '/jenna-analytic'
|
||||
path: '/jenna-analytic'
|
||||
fullPath: '/jenna-analytic'
|
||||
preLoaderRoute: typeof JennaAnalyticRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/demografi-pekerjaan': {
|
||||
id: '/demografi-pekerjaan'
|
||||
path: '/demografi-pekerjaan'
|
||||
fullPath: '/demografi-pekerjaan'
|
||||
preLoaderRoute: typeof DemografiPekerjaanRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/bumdes': {
|
||||
id: '/bumdes'
|
||||
path: '/bumdes'
|
||||
fullPath: '/bumdes'
|
||||
preLoaderRoute: typeof BumdesRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/bantuan': {
|
||||
id: '/bantuan'
|
||||
path: '/bantuan'
|
||||
fullPath: '/bantuan'
|
||||
preLoaderRoute: typeof BantuanRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/pengaturan': {
|
||||
id: '/pengaturan'
|
||||
path: '/pengaturan'
|
||||
fullPath: '/pengaturan'
|
||||
preLoaderRoute: typeof PengaturanRouteRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin': {
|
||||
@@ -429,13 +472,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ProfileIndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/dashboard/': {
|
||||
id: '/dashboard/'
|
||||
path: '/'
|
||||
fullPath: '/dashboard/'
|
||||
preLoaderRoute: typeof DashboardIndexRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
}
|
||||
'/admin/': {
|
||||
id: '/admin/'
|
||||
path: '/'
|
||||
@@ -457,68 +493,33 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ProfileEditRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/dashboard/sosial': {
|
||||
id: '/dashboard/sosial'
|
||||
path: '/sosial'
|
||||
fullPath: '/dashboard/sosial'
|
||||
preLoaderRoute: typeof DashboardSosialRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
'/pengaturan/umum': {
|
||||
id: '/pengaturan/umum'
|
||||
path: '/umum'
|
||||
fullPath: '/pengaturan/umum'
|
||||
preLoaderRoute: typeof PengaturanUmumRouteImport
|
||||
parentRoute: typeof PengaturanRouteRoute
|
||||
}
|
||||
'/dashboard/pengaduan-layanan-publik': {
|
||||
id: '/dashboard/pengaduan-layanan-publik'
|
||||
path: '/pengaduan-layanan-publik'
|
||||
fullPath: '/dashboard/pengaduan-layanan-publik'
|
||||
preLoaderRoute: typeof DashboardPengaduanLayananPublikRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
'/pengaturan/notifikasi': {
|
||||
id: '/pengaturan/notifikasi'
|
||||
path: '/notifikasi'
|
||||
fullPath: '/pengaturan/notifikasi'
|
||||
preLoaderRoute: typeof PengaturanNotifikasiRouteImport
|
||||
parentRoute: typeof PengaturanRouteRoute
|
||||
}
|
||||
'/dashboard/kinerja-divisi': {
|
||||
id: '/dashboard/kinerja-divisi'
|
||||
path: '/kinerja-divisi'
|
||||
fullPath: '/dashboard/kinerja-divisi'
|
||||
preLoaderRoute: typeof DashboardKinerjaDivisiRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
}
|
||||
'/dashboard/keuangan-anggaran': {
|
||||
id: '/dashboard/keuangan-anggaran'
|
||||
path: '/keuangan-anggaran'
|
||||
fullPath: '/dashboard/keuangan-anggaran'
|
||||
preLoaderRoute: typeof DashboardKeuanganAnggaranRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
}
|
||||
'/dashboard/keamanan': {
|
||||
id: '/dashboard/keamanan'
|
||||
'/pengaturan/keamanan': {
|
||||
id: '/pengaturan/keamanan'
|
||||
path: '/keamanan'
|
||||
fullPath: '/dashboard/keamanan'
|
||||
preLoaderRoute: typeof DashboardKeamananRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
fullPath: '/pengaturan/keamanan'
|
||||
preLoaderRoute: typeof PengaturanKeamananRouteImport
|
||||
parentRoute: typeof PengaturanRouteRoute
|
||||
}
|
||||
'/dashboard/jenna-analytic': {
|
||||
id: '/dashboard/jenna-analytic'
|
||||
path: '/jenna-analytic'
|
||||
fullPath: '/dashboard/jenna-analytic'
|
||||
preLoaderRoute: typeof DashboardJennaAnalyticRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
}
|
||||
'/dashboard/demografi-pekerjaan': {
|
||||
id: '/dashboard/demografi-pekerjaan'
|
||||
path: '/demografi-pekerjaan'
|
||||
fullPath: '/dashboard/demografi-pekerjaan'
|
||||
preLoaderRoute: typeof DashboardDemografiPekerjaanRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
}
|
||||
'/dashboard/bumdes': {
|
||||
id: '/dashboard/bumdes'
|
||||
path: '/bumdes'
|
||||
fullPath: '/dashboard/bumdes'
|
||||
preLoaderRoute: typeof DashboardBumdesRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
}
|
||||
'/dashboard/bantuan': {
|
||||
id: '/dashboard/bantuan'
|
||||
path: '/bantuan'
|
||||
fullPath: '/dashboard/bantuan'
|
||||
preLoaderRoute: typeof DashboardBantuanRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
'/pengaturan/akses-dan-tim': {
|
||||
id: '/pengaturan/akses-dan-tim'
|
||||
path: '/akses-dan-tim'
|
||||
fullPath: '/pengaturan/akses-dan-tim'
|
||||
preLoaderRoute: typeof PengaturanAksesDanTimRouteImport
|
||||
parentRoute: typeof PengaturanRouteRoute
|
||||
}
|
||||
'/admin/users': {
|
||||
id: '/admin/users'
|
||||
@@ -541,41 +542,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AdminApikeyRouteImport
|
||||
parentRoute: typeof AdminRouteRoute
|
||||
}
|
||||
'/dashboard/pengaturan': {
|
||||
id: '/dashboard/pengaturan'
|
||||
path: '/pengaturan'
|
||||
fullPath: '/dashboard/pengaturan'
|
||||
preLoaderRoute: typeof DashboardPengaturanRouteRouteImport
|
||||
parentRoute: typeof DashboardRouteRoute
|
||||
}
|
||||
'/dashboard/pengaturan/umum': {
|
||||
id: '/dashboard/pengaturan/umum'
|
||||
path: '/umum'
|
||||
fullPath: '/dashboard/pengaturan/umum'
|
||||
preLoaderRoute: typeof DashboardPengaturanUmumRouteImport
|
||||
parentRoute: typeof DashboardPengaturanRouteRoute
|
||||
}
|
||||
'/dashboard/pengaturan/notifikasi': {
|
||||
id: '/dashboard/pengaturan/notifikasi'
|
||||
path: '/notifikasi'
|
||||
fullPath: '/dashboard/pengaturan/notifikasi'
|
||||
preLoaderRoute: typeof DashboardPengaturanNotifikasiRouteImport
|
||||
parentRoute: typeof DashboardPengaturanRouteRoute
|
||||
}
|
||||
'/dashboard/pengaturan/keamanan': {
|
||||
id: '/dashboard/pengaturan/keamanan'
|
||||
path: '/keamanan'
|
||||
fullPath: '/dashboard/pengaturan/keamanan'
|
||||
preLoaderRoute: typeof DashboardPengaturanKeamananRouteImport
|
||||
parentRoute: typeof DashboardPengaturanRouteRoute
|
||||
}
|
||||
'/dashboard/pengaturan/akses-dan-tim': {
|
||||
id: '/dashboard/pengaturan/akses-dan-tim'
|
||||
path: '/akses-dan-tim'
|
||||
fullPath: '/dashboard/pengaturan/akses-dan-tim'
|
||||
preLoaderRoute: typeof DashboardPengaturanAksesDanTimRouteImport
|
||||
parentRoute: typeof DashboardPengaturanRouteRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,64 +563,39 @@ const AdminRouteRouteWithChildren = AdminRouteRoute._addFileChildren(
|
||||
AdminRouteRouteChildren,
|
||||
)
|
||||
|
||||
interface DashboardPengaturanRouteRouteChildren {
|
||||
DashboardPengaturanAksesDanTimRoute: typeof DashboardPengaturanAksesDanTimRoute
|
||||
DashboardPengaturanKeamananRoute: typeof DashboardPengaturanKeamananRoute
|
||||
DashboardPengaturanNotifikasiRoute: typeof DashboardPengaturanNotifikasiRoute
|
||||
DashboardPengaturanUmumRoute: typeof DashboardPengaturanUmumRoute
|
||||
interface PengaturanRouteRouteChildren {
|
||||
PengaturanAksesDanTimRoute: typeof PengaturanAksesDanTimRoute
|
||||
PengaturanKeamananRoute: typeof PengaturanKeamananRoute
|
||||
PengaturanNotifikasiRoute: typeof PengaturanNotifikasiRoute
|
||||
PengaturanUmumRoute: typeof PengaturanUmumRoute
|
||||
}
|
||||
|
||||
const DashboardPengaturanRouteRouteChildren: DashboardPengaturanRouteRouteChildren =
|
||||
{
|
||||
DashboardPengaturanAksesDanTimRoute: DashboardPengaturanAksesDanTimRoute,
|
||||
DashboardPengaturanKeamananRoute: DashboardPengaturanKeamananRoute,
|
||||
DashboardPengaturanNotifikasiRoute: DashboardPengaturanNotifikasiRoute,
|
||||
DashboardPengaturanUmumRoute: DashboardPengaturanUmumRoute,
|
||||
}
|
||||
|
||||
const DashboardPengaturanRouteRouteWithChildren =
|
||||
DashboardPengaturanRouteRoute._addFileChildren(
|
||||
DashboardPengaturanRouteRouteChildren,
|
||||
)
|
||||
|
||||
interface DashboardRouteRouteChildren {
|
||||
DashboardPengaturanRouteRoute: typeof DashboardPengaturanRouteRouteWithChildren
|
||||
DashboardBantuanRoute: typeof DashboardBantuanRoute
|
||||
DashboardBumdesRoute: typeof DashboardBumdesRoute
|
||||
DashboardDemografiPekerjaanRoute: typeof DashboardDemografiPekerjaanRoute
|
||||
DashboardJennaAnalyticRoute: typeof DashboardJennaAnalyticRoute
|
||||
DashboardKeamananRoute: typeof DashboardKeamananRoute
|
||||
DashboardKeuanganAnggaranRoute: typeof DashboardKeuanganAnggaranRoute
|
||||
DashboardKinerjaDivisiRoute: typeof DashboardKinerjaDivisiRoute
|
||||
DashboardPengaduanLayananPublikRoute: typeof DashboardPengaduanLayananPublikRoute
|
||||
DashboardSosialRoute: typeof DashboardSosialRoute
|
||||
DashboardIndexRoute: typeof DashboardIndexRoute
|
||||
const PengaturanRouteRouteChildren: PengaturanRouteRouteChildren = {
|
||||
PengaturanAksesDanTimRoute: PengaturanAksesDanTimRoute,
|
||||
PengaturanKeamananRoute: PengaturanKeamananRoute,
|
||||
PengaturanNotifikasiRoute: PengaturanNotifikasiRoute,
|
||||
PengaturanUmumRoute: PengaturanUmumRoute,
|
||||
}
|
||||
|
||||
const DashboardRouteRouteChildren: DashboardRouteRouteChildren = {
|
||||
DashboardPengaturanRouteRoute: DashboardPengaturanRouteRouteWithChildren,
|
||||
DashboardBantuanRoute: DashboardBantuanRoute,
|
||||
DashboardBumdesRoute: DashboardBumdesRoute,
|
||||
DashboardDemografiPekerjaanRoute: DashboardDemografiPekerjaanRoute,
|
||||
DashboardJennaAnalyticRoute: DashboardJennaAnalyticRoute,
|
||||
DashboardKeamananRoute: DashboardKeamananRoute,
|
||||
DashboardKeuanganAnggaranRoute: DashboardKeuanganAnggaranRoute,
|
||||
DashboardKinerjaDivisiRoute: DashboardKinerjaDivisiRoute,
|
||||
DashboardPengaduanLayananPublikRoute: DashboardPengaduanLayananPublikRoute,
|
||||
DashboardSosialRoute: DashboardSosialRoute,
|
||||
DashboardIndexRoute: DashboardIndexRoute,
|
||||
}
|
||||
|
||||
const DashboardRouteRouteWithChildren = DashboardRouteRoute._addFileChildren(
|
||||
DashboardRouteRouteChildren,
|
||||
const PengaturanRouteRouteWithChildren = PengaturanRouteRoute._addFileChildren(
|
||||
PengaturanRouteRouteChildren,
|
||||
)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AdminRouteRoute: AdminRouteRouteWithChildren,
|
||||
DashboardRouteRoute: DashboardRouteRouteWithChildren,
|
||||
PengaturanRouteRoute: PengaturanRouteRouteWithChildren,
|
||||
BantuanRoute: BantuanRoute,
|
||||
BumdesRoute: BumdesRoute,
|
||||
DemografiPekerjaanRoute: DemografiPekerjaanRoute,
|
||||
JennaAnalyticRoute: JennaAnalyticRoute,
|
||||
KeamananRoute: KeamananRoute,
|
||||
KeuanganAnggaranRoute: KeuanganAnggaranRoute,
|
||||
KinerjaDivisiRoute: KinerjaDivisiRoute,
|
||||
PengaduanLayananPublikRoute: PengaduanLayananPublikRoute,
|
||||
SigninRoute: SigninRoute,
|
||||
SignupRoute: SignupRoute,
|
||||
SosialRoute: SosialRoute,
|
||||
ProfileEditRoute: ProfileEditRoute,
|
||||
UsersIdRoute: UsersIdRoute,
|
||||
ProfileIndexRoute: ProfileIndexRoute,
|
||||
|
||||
@@ -7,8 +7,20 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
beforeLoad: protectedRouteMiddleware,
|
||||
onEnter({ context }) {
|
||||
beforeLoad: async ({ location }) => {
|
||||
// Only apply auth middleware for routes that need it
|
||||
// Public routes: /, /signin, /signup
|
||||
const isPublicRoute =
|
||||
location.pathname === "/" ||
|
||||
location.pathname === "/signin" ||
|
||||
location.pathname === "/signup";
|
||||
|
||||
if (isPublicRoute) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply protected route middleware for all other routes
|
||||
const context = await protectedRouteMiddleware({ location });
|
||||
authStore.user = context?.user as any;
|
||||
authStore.session = context?.session as any;
|
||||
},
|
||||
|
||||
@@ -154,7 +154,6 @@ function DashboardLayout() {
|
||||
</Group>
|
||||
|
||||
<Group gap="md">
|
||||
|
||||
<Menu
|
||||
shadow="md"
|
||||
width={200}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||
import { Header } from "@/components/header";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Header } from "@/components/header";
|
||||
import HelpPage from "@/components/help-page";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
export const Route = createFileRoute("/dashboard")({
|
||||
component: RouteComponent,
|
||||
export const Route = createFileRoute("/bantuan")({
|
||||
component: BantuanPage,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
function BantuanPage() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === 'dark' ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === 'dark' ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === 'dark' ? "#11192D" : "#edf3f8ff";
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
@@ -31,14 +33,18 @@ function RouteComponent() {
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar p="md" bg={navbarBgColor} style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<Outlet />
|
||||
<HelpPage />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
9
src/routes/bumdes.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/bumdes")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/bumdes"!</div>;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import HelpPage from '@/components/help-page'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/bantuan')({
|
||||
component: HelpPage,
|
||||
})
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import BumdesPage from '@/components/bumdes-page'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/bumdes')({
|
||||
component: BumdesPage,
|
||||
})
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import DemografiPekerjaan from '../../components/demografi-pekerjaan'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/demografi-pekerjaan')({
|
||||
component: DemografiPekerjaan,
|
||||
})
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { DashboardContent } from "@/components/dashboard-content";
|
||||
export const Route = createFileRoute("/dashboard/")({
|
||||
component: DashboardContent,
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import JennaAnalytic from '@/components/jenna-analytic'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/jenna-analytic')({
|
||||
component: JennaAnalytic,
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import KeamananPage from '@/components/keamanan-page'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/keamanan')({
|
||||
component: KeamananPage,
|
||||
})
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import KeuanganAnggaran from '@/components/keuangan-anggaran'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/keuangan-anggaran')({
|
||||
component: KeuanganAnggaran,
|
||||
})
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import KinerjaDivisi from "@/components/kinerja-divisi";
|
||||
export const Route = createFileRoute("/dashboard/kinerja-divisi")({
|
||||
component: KinerjaDivisi,
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
|
||||
export const Route = createFileRoute("/dashboard/pengaduan-layanan-publik")({
|
||||
component: PengaduanLayananPublik,
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import AksesDanTimSettings from '@/components/pengaturan/akses-dan-tim'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/pengaturan/akses-dan-tim')({
|
||||
component: AksesDanTimSettings,
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import KeamananSettings from '@/components/pengaturan/keamanan'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/pengaturan/keamanan')({
|
||||
component: KeamananSettings,
|
||||
})
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import NotifikasiSettings from '@/components/pengaturan/notifikasi'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/pengaturan/notifikasi')({
|
||||
component: NotifikasiSettings,
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/dashboard/pengaturan')({
|
||||
component: () => (
|
||||
<div className="p-2">
|
||||
<Outlet />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
import UmumSettings from '@/components/pengaturan/umum'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/dashboard/pengaturan/umum')({
|
||||
component: UmumSettings,
|
||||
})
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import SocialPage from '@/components/sosial-page'
|
||||
|
||||
|
||||
export const Route = createFileRoute('/dashboard/sosial')({
|
||||
component: SocialPage,
|
||||
})
|
||||
|
||||
51
src/routes/demografi-pekerjaan.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Header } from "@/components/header";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
import DemografiPekerjaan from "../components/demografi-pekerjaan";
|
||||
|
||||
export const Route = createFileRoute("/demografi-pekerjaan")({
|
||||
component: DemografiPekerjaanPage,
|
||||
});
|
||||
|
||||
function DemografiPekerjaanPage() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<DemografiPekerjaan />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
@@ -1,788 +1,51 @@
|
||||
import {
|
||||
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";
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { DashboardContent } from "@/components/dashboard-content";
|
||||
import { Header } from "@/components/header";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomePage,
|
||||
component: DashboardPage,
|
||||
});
|
||||
|
||||
// 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);
|
||||
}, []);
|
||||
function DashboardPage() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
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",
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<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>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<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"
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
{(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>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<DashboardContent />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
9
src/routes/jenna-analytic.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/jenna-analytic")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/jenna-analytic"!</div>;
|
||||
}
|
||||
9
src/routes/keamanan.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/keamanan")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/keamanan"!</div>;
|
||||
}
|
||||
51
src/routes/keuangan-anggaran.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Header } from "@/components/header";
|
||||
import KeuanganAnggaran from "@/components/keuangan-anggaran";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
export const Route = createFileRoute("/keuangan-anggaran")({
|
||||
component: KeuanganAnggaranPage,
|
||||
});
|
||||
|
||||
function KeuanganAnggaranPage() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<KeuanganAnggaran />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
51
src/routes/kinerja-divisi.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Header } from "@/components/header";
|
||||
import KinerjaDivisi from "@/components/kinerja-divisi";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
export const Route = createFileRoute("/kinerja-divisi")({
|
||||
component: KinerjaDivisiPage,
|
||||
});
|
||||
|
||||
function KinerjaDivisiPage() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<KinerjaDivisi />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
51
src/routes/pengaduan-layanan-publik.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Header } from "@/components/header";
|
||||
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
|
||||
import { Sidebar } from "@/components/sidebar";
|
||||
|
||||
export const Route = createFileRoute("/pengaduan-layanan-publik")({
|
||||
component: PengaduanLayananPublikPage,
|
||||
});
|
||||
|
||||
function PengaduanLayananPublikPage() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
|
||||
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
|
||||
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<PengaduanLayananPublik />
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
9
src/routes/pengaturan/akses-dan-tim.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/pengaturan/akses-dan-tim")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/pengaturan/akses-dan-tim"!</div>;
|
||||
}
|
||||
9
src/routes/pengaturan/keamanan.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/pengaturan/keamanan")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/pengaturan/keamanan"!</div>;
|
||||
}
|
||||
9
src/routes/pengaturan/notifikasi.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/pengaturan/notifikasi")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/pengaturan/notifikasi"!</div>;
|
||||
}
|
||||
68
src/routes/pengaturan/route.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { AppShell, Burger, Group, useMantineColorScheme } 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 { Sidebar } from "@/components/sidebar";
|
||||
|
||||
export const Route = createFileRoute("/pengaturan")({
|
||||
component: PengaturanLayout,
|
||||
});
|
||||
|
||||
function PengaturanLayout() {
|
||||
const [opened, { toggle, close }] = useDisclosure();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 48em)");
|
||||
const routerState = useRouterState();
|
||||
|
||||
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, isMobile, opened, close]);
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened },
|
||||
}}
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="lg" align="center" wrap="nowrap">
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
<AppShell.Navbar
|
||||
p="md"
|
||||
bg={navbarBgColor}
|
||||
style={{ display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
<Sidebar />
|
||||
</div>
|
||||
</AppShell.Navbar>
|
||||
|
||||
<AppShell.Main bg={mainBgColor}>
|
||||
<div className="p-2">
|
||||
<Outlet />
|
||||
</div>
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
6
src/routes/pengaturan/umum.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import UmumSettings from "@/components/pengaturan/umum";
|
||||
|
||||
export const Route = createFileRoute("/pengaturan/umum")({
|
||||
component: UmumSettings,
|
||||
});
|
||||
9
src/routes/sosial.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/sosial")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/sosial"!</div>;
|
||||
}
|
||||
@@ -2,11 +2,7 @@ import createClient from "openapi-fetch";
|
||||
import type { paths } from "../../generated/api";
|
||||
import { VITE_PUBLIC_URL } from "./env";
|
||||
|
||||
const baseUrl =
|
||||
VITE_PUBLIC_URL ||
|
||||
(typeof window !== "undefined"
|
||||
? window.location.origin
|
||||
: "http://localhost:3000");
|
||||
const baseUrl = VITE_PUBLIC_URL;
|
||||
|
||||
export const apiClient = createClient<paths>({
|
||||
baseUrl: baseUrl,
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { prismaAdapter } from "better-auth/adapters/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();
|
||||
|
||||
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');
|
||||
export const auth = betterAuth({
|
||||
baseURL: baseUrl,
|
||||
@@ -26,6 +21,7 @@ export const auth = betterAuth({
|
||||
clientId: process.env.GITHUB_CLIENT_ID || "CLIENT_ID_MISSING",
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET || "CLIENT_SECRET_MISSING",
|
||||
enabled: true,
|
||||
redirectURI: `${baseUrl}/api/auth/callback/github`,
|
||||
},
|
||||
},
|
||||
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,
|
||||
trustedOrigins: ["http://localhost:5173", "http://localhost:3000"],
|
||||
session: {
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
@@ -48,5 +60,6 @@ export const auth = betterAuth({
|
||||
},
|
||||
advanced: {
|
||||
cookiePrefix: "bun-react",
|
||||
trustProxy: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,10 +20,21 @@ export const getEnv = (key: string, defaultValue = ""): string => {
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export const VITE_PUBLIC_URL = getEnv(
|
||||
"VITE_PUBLIC_URL",
|
||||
"http://localhost:3000",
|
||||
);
|
||||
export const VITE_PUBLIC_URL = (() => {
|
||||
// Priority:
|
||||
// 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 = (() => {
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { createServer as createViteServer } from "vite";
|
||||
export async function createVite() {
|
||||
return createViteServer({
|
||||
root: process.cwd(),
|
||||
publicDir: "public",
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(process.cwd(), "./src"),
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
content: [
|
||||
"./src/index.html",
|
||||
"./public/**/*.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"darmasaba-navy": {
|
||||
DEFAULT: "#1E3A5F", // Primary navy color
|
||||
DEFAULT: "#1E3A5F",
|
||||
50: "#E1E4F2",
|
||||
100: "#B9C2DD",
|
||||
200: "#91A0C9",
|
||||
@@ -18,7 +22,7 @@ module.exports = {
|
||||
900: "#071833",
|
||||
},
|
||||
"darmasaba-blue": {
|
||||
DEFAULT: "#3B82F6", // Primary blue color
|
||||
DEFAULT: "#3B82F6",
|
||||
50: "#E3F0FF",
|
||||
100: "#B6D9FF",
|
||||
200: "#89C2FF",
|
||||
|
||||