Compare commits

...

33 Commits

Author SHA1 Message Date
0a97e31416 Fix New UI Pengaduan 2026-03-18 00:34:53 +07:00
158a2db435 Fix New UI Pengaduan 2026-03-17 21:41:03 +07:00
2d68d4dc06 Fix New UI Kinerja Divisi 2026-03-17 21:19:10 +07:00
97e6caa332 Fix UI Beranda 2026-03-17 21:03:36 +07:00
f0c37272b9 Progress Tampilan UI Dashboard Desa Plus NOC 2026-03-17 20:53:33 +07:00
8c35d58b38 refactor: modularize kinerja-divisi components per PromptDashboard.md
- Create ActivityCard component for program kegiatan cards
- Create DivisionList component for divisi teraktif with arrow icons
- Create DocumentChart component for bar chart (jumlah dokumen)
- Create ProgressChart component for pie chart (progres kegiatan)
- Create DiscussionPanel component for diskusi internal
- Create EventCard component for agenda hari ini
- Create ArchiveCard component for arsip digital perangkat desa
- Refactor main KinerjaDivisi component to use new modular components
- Implement responsive 3-column grid layout
- Add proper dark mode support with specified colors
- Add hover effects and smooth animations

New components structure:
  src/components/kinerja-divisi/
    - activity-card.tsx
    - archive-card.tsx
    - discussion-panel.tsx
    - division-list.tsx
    - document-chart.tsx
    - event-card.tsx
    - progress-chart.tsx
    - index.ts (exports)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-13 16:47:19 +08:00
952f7ecb16 refactor: modularize dashboard components per PromptDashboard.md
- Create reusable StatCard component for header metrics
- Create ChartSurat component for bar chart (surat statistics)
- Create DivisionProgress component for divisi teraktif
- Create ChartAPBDes component for APBDes horizontal bar chart
- Create ActivityList component for calendar events
- Create SatisfactionChart component for donut chart
- Create SDGSCard component for SDGs metrics
- Refactor DashboardContent to use new modular components
- Add proper dark mode support with specified colors
- Implement responsive grid layout (12/6/1 columns)
- Add custom SDGs icons (Energy, Peace, Health, Poverty, Ocean)

New components structure:
  src/components/dashboard/
    - stat-card.tsx
    - chart-surat.tsx
    - chart-apbdes.tsx
    - division-progress.tsx
    - activity-list.tsx
    - satisfaction-chart.tsx
    - sdgs-card.tsx
    - index.ts (exports)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-13 15:36:31 +08:00
a74e0c02e5 fix: resolve merge conflict in pengaturan/route.tsx
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-13 14:41:22 +08:00
17ecd3feca fix: make dashboard public and remove admin-only restriction from main pages
- Make homepage (/) accessible without authentication
- Allow all authenticated users (user & admin) to access main pages:
  - /kinerja-divisi, /pengaduan, /jenna, /demografi
  - /keuangan, /bumdes, /sosial, /keamanan
  - /bantuan, /pengaturan
- Reserve admin-only access for /admin/* routes
- Update auth middleware to handle public routes properly

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-13 14:21:30 +08:00
d88cf2b100 Merge pull request #5 from bipprojectbali/nico/26-feb-26/fix-seed-2
Fix Gambar
2026-03-12 15:21:17 +08:00
e0955ed2c4 Merge branch 'stg' into nico/26-feb-26/fix-seed-2 2026-03-12 15:21:05 +08:00
918399bf62 Fix Gambar 2026-03-12 15:16:41 +08:00
7ce2eb6ae8 Merge pull request #4 from bipprojectbali/nico/27-feb-26/fix-gambar
Add Deploy
2026-03-12 14:47:12 +08:00
40772859f9 Merge branch 'stg' into nico/27-feb-26/fix-gambar 2026-03-12 14:47:03 +08:00
c7b34b8c28 Add Deploy 2026-03-12 14:40:15 +08:00
9bf73a305c Merge pull request #3 from bipprojectbali/fix-ui-berantakan
fix: production build CSS dan responsive layout untuk staging
2026-03-12 12:19:33 +08:00
947adc1537 fix: production build CSS dan responsive layout untuk staging
- Tambah scripts/build.ts untuk build CSS via PostCSS/Tailwind
- Update package.json build script untuk gunakan build script baru
- Fix responsive grid di sosial-page (lg -> md breakpoint)
- Tambah padding responsive untuk mobile display
- Convert inline styles ke Tailwind classes untuk konsistensi
- Update tailwind.config.js content paths
- Tambah CSS variables di index.css untuk color palette
- Update Dockerfile untuk gunakan build script baru

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

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

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

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

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

19
.env.example Normal file
View File

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

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

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

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

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

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

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

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

@@ -0,0 +1,93 @@
#!/bin/bash
: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}"
: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}"
: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}"
: "${STACK_NAME:?STACK_NAME tidak di-set}"
echo "🔐 Autentikasi ke Portainer..."
TOKEN=$(curl -s -X POST https://${PORTAINER_URL}/api/auth \
-H "Content-Type: application/json" \
-d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \
| jq -r .jwt)
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "❌ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD."
exit 1
fi
echo "🔍 Mencari stack: $STACK_NAME..."
STACK=$(curl -s -X GET https://${PORTAINER_URL}/api/stacks \
-H "Authorization: Bearer ${TOKEN}" \
| jq ".[] | select(.Name == \"$STACK_NAME\")")
if [ -z "$STACK" ]; then
echo "❌ Stack '$STACK_NAME' tidak ditemukan di Portainer!"
echo " Pastikan nama stack sudah benar."
exit 1
fi
STACK_ID=$(echo "$STACK" | jq -r .Id)
ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId)
ENV=$(echo "$STACK" | jq '.Env // []')
echo "📄 Mengambil compose file..."
STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \
-H "Authorization: Bearer ${TOKEN}" \
| jq -r .StackFileContent)
PAYLOAD=$(jq -n \
--arg content "$STACK_FILE" \
--argjson env "$ENV" \
'{stackFileContent: $content, env: $env, pullImage: true}')
echo "🚀 Redeploying $STACK_NAME (pull latest image)..."
HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \
-X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
if [ "$HTTP_STATUS" != "200" ]; then
echo "❌ Redeploy gagal! HTTP Status: $HTTP_STATUS"
cat /tmp/portainer_response.json | jq .
exit 1
fi
echo "⏳ Menunggu container running..."
MAX_RETRY=15
COUNT=0
while [ $COUNT -lt $MAX_RETRY ]; do
sleep 5
COUNT=$((COUNT + 1))
CONTAINERS=$(curl -s -X GET \
"https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \
-H "Authorization: Bearer ${TOKEN}")
TOTAL=$(echo "$CONTAINERS" | jq 'length')
RUNNING=$(echo "$CONTAINERS" | jq '[.[] | select(.State == "running")] | length')
FAILED=$(echo "$CONTAINERS" | jq '[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not))] | length')
echo "🔄 [${COUNT}/${MAX_RETRY}] Running: ${RUNNING} | Failed: ${FAILED} | Total: ${TOTAL}"
echo "$CONTAINERS" | jq -r '.[] | " → \(.Names[0]) | \(.State) | \(.Status)"'
if [ "$FAILED" -gt "0" ]; then
echo ""
echo "❌ Ada container yang crash!"
echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not)) | " → \(.Names[0]) | \(.Status)"'
exit 1
fi
if [ "$RUNNING" -gt "0" ]; then
echo ""
echo "✅ Stack $STACK_NAME berhasil di-redeploy dan running!"
exit 0
fi
done
echo ""
echo "❌ Timeout! Stack tidak kunjung running setelah $((MAX_RETRY * 5)) detik."
exit 1

7
.gitignore vendored
View File

@@ -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,12 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Finder (MacOS) folder config
.DS_Store
# Dashboard-MD
Dashboard-MD
# md
*.md
# Playwright artifacts
test-results/
playwright-report/

62
Dockerfile Normal file
View File

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

168
Pengaduan-New.md Normal file
View File

@@ -0,0 +1,168 @@
Create a modern analytics dashboard UI for a village complaint system (Pengaduan Dashboard).
Tech stack:
- React 19 + Vite (Bun runtime)
- Mantine UI (core components)
- TailwindCSS (layout & spacing only)
- Recharts (charts)
- TanStack Router
- Icons: lucide-react
- State: Valtio
- Date: dayjs
---
## 🎨 DESIGN STYLE
- Clean, minimal, and soft dashboard
- Background: light gray (#f3f4f6)
- Card: white with subtle shadow
- Border radius: 16px24px (rounded-2xl)
- Typography: medium contrast (not too bold)
- Primary color: navy blue (#1E3A5F)
- Accent: soft blue + neutral gray
- Icons inside circular solid background
Spacing:
- Use gap-6 consistently
- Internal padding: p-5 or p-6
- Layout must feel breathable (no clutter)
---
## 🧱 LAYOUT STRUCTURE
### 🔹 TOP SECTION (4 STAT CARDS - GRID)
Grid: 4 columns (responsive → 2 / 1)
Each card contains:
- Title (small, muted)
- Big number (bold, large)
- Subtitle (small gray text)
- Right side: circular icon container
Example:
- Total Pengaduan → 42 → "Bulan ini"
- Baru → 14 → "Belum diproses"
- Diproses → 14 → "Sedang ditangani"
- Selesai → 14 → "Terselesaikan"
Use:
- Mantine Card
- Group justify="space-between"
- Icon inside circle (bg navy, icon white)
---
## 📈 MAIN CHART (FULL WIDTH)
Title: "Tren Pengaduan"
- Use Recharts LineChart
- Smooth line (monotone)
- Show dots on each point
- Data: Apr → Okt
- Value range: 3060
Style:
- Minimal grid (light dashed)
- No heavy colors (use gray/blue line)
- Rounded container card
- Add small top-right icon (expand)
---
## 📊 BOTTOM SECTION (3 COLUMN GRID)
### 🔹 LEFT: "Surat Terbanyak"
- Horizontal bar chart (Recharts)
- Categories:
- KTP
- KK
- Domisili
- Usaha
- Lainnya
Style:
- Dark blue bars
- Rounded edges
- Clean axis
---
### 🔹 CENTER: "Pengajuan Terbaru"
List of activity cards:
Each item:
- Name (bold)
- Subtitle (jenis surat)
- Time (small text)
- Status badge (kanan)
Status:
- baru → red
- proses → blue
- selesai → green
Style:
- Card per item
- Soft border
- Rounded
- Compact spacing
---
### 🔹 RIGHT: "Ajuan Ide Inovatif"
List mirip dengan pengajuan terbaru:
Each item:
- Nama
- Judul ide
- Waktu
- Button kecil "Detail"
Style:
- Right-aligned action button
- Light border
- Clean spacing
---
## ⚙️ COMPONENT STRUCTURE
components/
- StatCard.tsx
- LineChartCard.tsx
- BarChartCard.tsx
- ActivityList.tsx
- IdeaList.tsx
routes/
- dashboard.tsx
---
## ✨ INTERACTIONS (IMPORTANT)
- Hover card → scale(1.02)
- Transition: 150ms ease
- Icon circle slightly pop on hover
- List item hover → subtle bg change
---
## 🎯 UX DETAILS
- Numbers must be visually dominant
- Icons must balance layout (not too big)
- Avoid heavy borders
- Keep everything aligned perfectly
- No clutter
---
## 🚀 OUTPUT
- Modular React components (NOT one file)
- Clean code (production-ready)
- Use Mantine properly (no hacky inline styles unless needed)
- Use Tailwind only for layout/grid/spacing

12
QWEN.md
View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

@@ -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=="],

View File

@@ -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",

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
public/SDGS-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
public/SDGS-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
public/SDGS-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
public/light-mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

58
scripts/build.ts Normal file
View File

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

View File

@@ -1,296 +1,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;

View File

@@ -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";

View File

@@ -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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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";

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -1,123 +1,79 @@
import React from "react";
import {
Button,
Card,
Badge,
Progress,
Title,
Text,
Group,
Stack,
Grid,
Box,
Card,
Grid,
GridCol,
Group,
Progress,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { BarChart } from "@mantine/charts";
import {
MessageCircle,
CheckCircle,
AlertTriangle,
Clock,
TrendingUp,
} from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
// Sample Data
// KPI Data
const kpiData = [
{
id: 1,
title: "Interaksi Hari Ini",
value: "61",
delta: "+15% dari kemarin",
deltaType: "positive",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H16.5m-13.5 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"
/>
</svg>
),
subtitle: "+15% dari kemarin",
trend: "positive",
icon: MessageCircle,
},
{
id: 2,
title: "Jawaban Otomatis",
value: "87%",
sub: "53 dari 61 interaksi",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12c0 1.268-.63 2.473-1.688 3.342-.48.485-.926.97-1.378 1.44c-1.472 1.58-2.306 2.787-2.91 3.514-.15.18-.207.33-.207.33A.75.75 0 0 1 15 21h-3c-1.104 0-2.08-.542-2.657-1.455-.139-.201-.264-.406-.38-.614l-.014-.025C8.85 18.067 8.156 17.2 7.5 16.325.728 12.56.728 7.44 7.5 3.675c3.04-.482 5.584.47 7.042 1.956.674.672 1.228 1.462 1.696 2.307.426.786.793 1.582 1.113 2.392h.001Z"
/>
</svg>
),
subtitle: "53 dari 61 interaksi",
icon: CheckCircle,
},
{
id: 3,
title: "Belum Ditindak",
value: "8",
sub: "Perlu respon manual",
deltaType: "negative",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
),
subtitle: "Perlu respon manual",
icon: AlertTriangle,
},
{
id: 4,
title: "Waktu Respon",
value: "2.3 sec",
sub: "Rata-rata",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6 text-muted-foreground"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
),
subtitle: "Rata-rata",
icon: Clock,
},
];
// Chart Data
const chartData = [
{ day: "Sen", total: 100 },
{ day: "Sel", total: 120 },
{ day: "Rab", total: 90 },
{ day: "Kam", total: 150 },
{ day: "Jum", total: 110 },
{ day: "Sab", total: 80 },
{ day: "Min", total: 130 },
{ day: "Sen", total: 45 },
{ day: "Sel", total: 62 },
{ day: "Rab", total: 38 },
{ day: "Kam", total: 75 },
{ day: "Jum", total: 58 },
{ day: "Sab", total: 32 },
{ day: "Min", total: 51 },
];
// Top Topics Data
const topTopics = [
{ topic: "Cara mengurus KTP", count: 89 },
{ topic: "Syarat Kartu Keluarga", count: 76 },
@@ -126,6 +82,7 @@ const topTopics = [
{ topic: "Info program bansos", count: 48 },
];
// Busy Hours Data
const busyHours = [
{ period: "Pagi (0812)", percentage: 30 },
{ period: "Siang (1216)", percentage: 40 },
@@ -138,125 +95,206 @@ const JennaAnalytic = () => {
const dark = colorScheme === "dark";
return (
<Box className="space-y-6">
<Stack gap="xl">
{/* KPI Cards */}
<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" }}>
<Group justify="space-between" align="flex-start" mb="xs">
<Text size="sm" fw={500} c="dimmed">
{kpi.title}
<Stack gap="lg">
{/* TOP SECTION - 4 STAT CARDS */}
<Grid gutter="md">
{kpiData.map((item) => (
<Grid.Col key={item.id} span={{ base: 12, sm: 6, lg: 3 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
h="100%"
>
<Group justify="space-between" align="flex-start" w="100%">
<Stack gap={2}>
<Text size="sm" c="dimmed">
{item.title}
</Text>
{React.cloneElement(kpi.icon, {
className: "h-6 w-6", // Keeping classes for now, can be replaced by Mantine Icon component if available or styled with sx prop
color: "var(--mantine-color-dimmed)", // Set color via prop
})}
</Group>
<Title order={3} fw={700} mt="xs">
{kpi.value}
</Title>
{kpi.delta && (
<Text
size="xs"
c={
kpi.deltaType === "positive"
? "green"
: kpi.deltaType === "negative"
? "red"
: "dimmed"
}
mt={4}
>
{kpi.delta}
<Text size="xl" fw={700} c={dark ? "white" : "gray.9"}>
{item.value}
</Text>
)}
{kpi.sub && (
<Text size="xs" c="dimmed" mt={2}>
{kpi.sub}
</Text>
)}
</Card>
</Grid.Col>
))}
</Grid>
<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>
<BarChart
h={300}
data={chartData}
dataKey="day"
series={[{ name: 'total', color: 'blue' }]}
withLegend
/>
</Card>
{/* Charts and Lists Section */}
<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%">
<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}
<Group gap={4} align="flex-start">
{item.trend === "positive" && (
<TrendingUp size={14} color="#22C55E" />
)}
<Text
size="xs"
c={
item.trend === "positive"
? "green"
: dark
? "gray.4"
: "gray.5"
}
>
{item.subtitle}
</Text>
<Group align="center">
<Progress value={item.percentage} flex={1} />
<Text size="sm" fw={500}>
{item.percentage}%
</Text>
</Group>
</Box>
))}
</Stack>
</Group>
</Stack>
<ThemeIcon
color="#1E3A5F"
variant="filled"
size="lg"
radius="xl"
>
<item.icon style={{ width: "60%", height: "60%" }} />
</ThemeIcon>
</Group>
</Card>
</Grid.Col>
))}
</Grid>
{/* Topik Pertanyaan Terbanyak & Jam Tersibuk */}
<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%">
<Title order={3} fw={500} mb="md">
Topik Pertanyaan Terbanyak
</Title>
<Stack gap="xs">
{topTopics.map((item, index) => (
<Group
key={index}
justify="space-between"
align="center"
p="xs"
{/* MAIN CHART - INTERAKSI CHATBOT */}
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
>
<Group justify="space-between" mb="md">
<Title order={4} c={dark ? "white" : "gray.9"}>
Interaksi Chatbot
</Title>
</Group>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<CartesianGrid
strokeDasharray="3 3"
vertical={false}
stroke={dark ? "#334155" : "#e5e7eb"}
/>
<XAxis
dataKey="day"
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" }}
cursor={{ fill: dark ? "#334155" : "#f3f4f6" }}
/>
<Bar
dataKey="total"
fill="#1E3A5F"
radius={[8, 8, 0, 0]}
maxBarSize={60}
/>
</BarChart>
</ResponsiveContainer>
</Card>
{/* BOTTOM SECTION - 2 COLUMNS */}
<Grid gutter="lg">
{/* LEFT: TOPIK PERTANYAAN TERBANYAK */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
Topik Pertanyaan Terbanyak
</Title>
<Stack gap="xs">
{topTopics.map((item, index) => (
<Box
key={index}
p="sm"
bg={dark ? "#334155" : "#F1F5F9"}
style={{
transition: "background-color 0.15s ease",
cursor: "pointer",
}}
>
<Group justify="space-between">
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
{item.topic}
</Text>
<Badge
variant="light"
color="darmasaba-blue"
radius="sm"
fw={600}
>
<Text size="sm" fw={500}>
{item.topic}
</Text>
<Badge variant="light" color="gray">
{item.count}x
</Badge>
</Group>
))}
</Stack>
</Card>
{/* Jam Tersibuk */}
{item.count}x
</Badge>
</Group>
</Box>
))}
</Stack>
</Grid.Col>
</Grid >
</Card>
</Grid.Col>
</Stack >
</Box >
{/* RIGHT: JAM TERSIBUK */}
<Grid.Col span={{ base: 12, lg: 6 }}>
<Card
p="md"
radius="xl"
withBorder
bg={dark ? "#1E293B" : "white"}
style={{
borderColor: dark ? "#334155" : "white",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
}}
h="100%"
>
<Title order={4} c={dark ? "white" : "gray.9"} mb="md">
Jam Tersibuk
</Title>
<Stack gap="md">
{busyHours.map((item, index) => (
<Box key={index}>
<Group justify="space-between" mb={5}>
<Text size="sm" fw={500} c={dark ? "white" : "gray.9"}>
{item.period}
</Text>
<Text size="sm" fw={600} c={dark ? "white" : "gray.9"}>
{item.percentage}%
</Text>
</Group>
<Progress
value={item.percentage}
size="lg"
radius="xl"
color="#1E3A5F"
animated
/>
</Box>
))}
</Stack>
</Card>
</Grid.Col>
</Grid>
</Stack>
);
};
}
export default JennaAnalytic;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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";

View 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>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,125 +1,190 @@
import { Card, Title, Text, Space, Button, Group, Alert, Table, ActionIcon, Modal, TextInput, Select, useMantineColorScheme } from '@mantine/core';
import { 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,49 @@ 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={dark ? "/white.png" : "/light-mode.png"}
alt="Logo"
/>
{/* Search */}
<Box p="md">
@@ -112,7 +96,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 +107,8 @@ export function Sidebar({ className }: SidebarProps) {
body: {
"&:hover": {
background: "#F1F5F9",
}
}
},
},
}}
/>
);
@@ -132,7 +118,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 +128,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 +139,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 +162,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 +173,8 @@ export function Sidebar({ className }: SidebarProps) {
body: {
"&:hover": {
background: "#F1F5F9",
}
}
},
},
}}
/>
);

View File

@@ -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;

View File

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

View File

@@ -1,90 +1,86 @@
import { Card, useMantineTheme, useComputedColorScheme } from '@mantine/core';
import type { CardProps } from '@mantine/core';
import type { 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>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -14,10 +14,9 @@ import { Inspector } from "react-dev-inspector";
import { createRoot } from "react-dom/client";
import { 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}</>;

View File

@@ -1 +1 @@
@import "tailwindcss";
@import "tailwindcss";

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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;
},

View File

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

View File

@@ -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
View 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>;
}

View File

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

View File

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

View File

@@ -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,
})

View File

@@ -1,5 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { DashboardContent } from "@/components/dashboard-content";
export const Route = createFileRoute("/dashboard/")({
component: DashboardContent,
});

View File

@@ -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,
})

View File

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

View File

@@ -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,
})

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,
})

View File

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

View File

@@ -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,
})

View File

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

View 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>
);
}

View File

@@ -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>
);
}

View 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 JennaAnalytic from "@/components/jenna-analytic";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/jenna-analytic")({
component: JennaAnalyticPage,
});
function JennaAnalyticPage() {
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}>
<JennaAnalytic />
</AppShell.Main>
</AppShell>
);
}

9
src/routes/keamanan.tsx Normal file
View 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>;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>;
}

View 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>;
}

View 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>;
}

View 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>
);
}

View 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
View 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>;
}

View File

@@ -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,

View File

@@ -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,
},
});

View File

@@ -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 {

View File

@@ -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"),

View File

@@ -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",