Compare commits

...

22 Commits

Author SHA1 Message Date
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
76 changed files with 2864 additions and 2341 deletions

19
.env.example Normal file
View File

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

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

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

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

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

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

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

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

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

4
.gitignore vendored
View File

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

62
Dockerfile Normal file
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"]

302
PromptDashboard.md Normal file
View File

@@ -0,0 +1,302 @@
Buat halaman dashboard admin modern untuk sistem pemerintahan desa bernama **Darmasaba Dashboard NOC**.
Gunakan stack berikut:
Frontend:
* React 19
* Bun runtime
* Vite
* TailwindCSS
* Mantine UI
* Mantine Charts atau Recharts
* Tabler Icons
* TanStack Router
* Dayjs
UI harus modular dengan reusable components.
Gunakan **TailwindCSS sebagai styling utama** dengan warna dari konfigurasi berikut:
Primary:
darmasaba-navy (#1E3A5F)
Secondary:
darmasaba-blue (#3B82F6)
Success:
#22C55E
Warning:
#FACC15
Danger:
#EF4444
Background:
#F5F8FB
Dashboard harus memiliki **Light Mode dan Dark Mode**.
Dark Mode Color Rules:
background: #0F172A
card: #1E293B
border: #334155
text: #E2E8F0
Card style:
* rounded-xl
* soft shadow
* padding besar
* border subtle
* smooth hover animation
Gunakan grid layout responsive.
---
SECTION 1 — PROGRAM KEGIATAN
Buat 4 card horizontal di bagian atas yang menampilkan kegiatan desa.
Setiap card memiliki:
* header biru
* progress bar kegiatan
* tanggal kegiatan
* badge status
Data card:
1.
Judul: Rakor 2025
Tanggal: 3 Juli 2025
Progress: 90%
Status: selesai
2.
Judul: Pemutakhiran Indeks Desa
Tanggal: 3 Juli 2025
Progress: 85%
Status: selesai
3.
Judul: Mengurus Akta Cerai Warga
Tanggal: 3 Juli 2025
Progress: 80%
Status: selesai
4.
Judul: Pasek 7 Desa Adat
Tanggal: 3 Juli 2025
Progress: 92%
Status: selesai
Progress bar:
* rounded
* warna warning
* animasi smooth
Status badge:
* success color
---
SECTION 2 — GRID DASHBOARD
Layout:
3 column grid.
Left column (sidebar style):
Divisi Teraktif
List item card dengan arrow icon.
Data:
Kesejahteraan — 37 kegiatan
Pemerintahan — 26 kegiatan
Keuangan — 17 kegiatan
Sekretaris Desa — 15 kegiatan
Tata Usaha TK — 14 kegiatan
Perangkat Kewilayahan — 12 kegiatan
Pelayanan — 10 kegiatan
Perencanaan — 9 kegiatan
Tata Usaha & Umum — 7 kegiatan
Setiap item:
* rounded
* hover effect
* arrow icon kanan
---
Middle column:
Jumlah Dokumen
Gunakan **Bar Chart**.
Kategori:
* Gambar
* Dokumen
Nilai:
* Gambar: 300
* Dokumen: 310
Gunakan:
Recharts atau Mantine Charts.
---
Right column:
Progres Kegiatan
Gunakan **Pie Chart**.
Data:
Selesai — 83.33%
Dikerjakan — 16.67%
Segera Dikerjakan — 0%
Dibatalkan — 0%
Legend harus berwarna.
---
SECTION 3 — DISCUSSION PANEL
Judul: Diskusi
Tampilkan list diskusi internal staf.
Item card memiliki:
* icon chat
* judul pesan
* nama pengirim
* tanggal
Contoh data:
"Kepada Pelayanan, mohon di cek..."
Pengirim: I.B Surya Prabhawa Manu
Tanggal: 12 Apr 2025
"Kepada staf perencanaan @suar..."
Pengirim: Ni Nyoman Yuliani
Tanggal: 14 Jun 2025
"ijin atau mohon kepada KBD sar..."
Pengirim: Ni Wayan Martini
Tanggal: 12 Apr 2025
---
SECTION 4 — ACARA HARI INI
Card sederhana.
Jika tidak ada acara tampilkan:
"Tidak ada acara hari ini"
---
SECTION 5 — ARSIP DIGITAL PERANGKAT DESA
Grid 2 column.
Menu arsip:
Surat Keputusan
Dokumentasi
Laporan Keuangan
Notulensi Rapat
Setiap item berupa card clickable dengan:
* icon dokumen
* border
* hover effect
---
DESIGN STYLE
Gunakan gaya:
Modern Government Dashboard
Clean UI
Soft shadow
Rounded-xl
Spacing besar
Minimalistic
---
RESPONSIVE RULES
Desktop:
12 column grid
Tablet:
6 column grid
Mobile:
single column stack
---
COMPONENT STRUCTURE
src/components/dashboard
activity-card.tsx
division-list.tsx
document-chart.tsx
progress-chart.tsx
discussion-panel.tsx
event-card.tsx
archive-card.tsx
src/pages
dashboard.tsx
---
CODE QUALITY
Gunakan:
* React hooks
* reusable components
* Mantine components jika perlu
* Tailwind utility classes
* dark mode support
* responsive layout
* clean TypeScript
---
Output:
* Halaman dashboard lengkap
* Semua komponen reusable
* Chart sudah bekerja
* Layout identik dengan desain dashboard modern pemerintahan

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

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

View File

@@ -133,7 +133,18 @@ async function main() {
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/logo-desa-plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

58
scripts/build.ts Normal file
View File

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

View File

@@ -1,510 +1,117 @@
import {
Calendar,
CheckCircle,
FileText,
MessageCircle,
Users,
} from "lucide-react";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Pie,
PieChart,
ResponsiveContainer,
Tooltip, // Added Tooltip import
XAxis,
YAxis,
} 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 {
ActionIcon,
Badge,
Box,
Card, // Added for icon containers
Grid,
Group,
Progress,
Stack,
Text,
ThemeIcon,
Title,
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 apbdesData = [
{ name: "Belanja", value: 70, color: "blue" },
{ name: "Pendapatan", value: 90, color: "green" },
{ name: "Pembangunan", value: 50, color: "orange" },
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";
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">
{apbdesData.map((data, index) => (
<Grid key={index} align="center">
<Grid.Col span={3}>
<Text size="sm" fw={500}>
{data.name}
</Text>
</Grid.Col>
<Grid.Col span={9}>
<Progress
value={data.value}
size="lg"
radius="xl"
color={data.color}
/>
</Grid.Col>
</Grid>
))}
</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,69 @@
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)",
}}
>
<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,74 @@
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)",
}}
>
<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,110 @@
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)",
}}
>
<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,69 @@
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)",
}}
>
<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,82 @@
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)",
}}
>
<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,72 +1,121 @@
import {
ActionIcon,
Avatar,
Badge,
Box,
Divider,
Group,
Text,
Title,
useMantineColorScheme,
} from "@mantine/core";
import { useLocation } from "@tanstack/react-router";
import { Bell, Moon, Sun } from "lucide-react";
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();
const title =
location.pathname === "/"
? "Desa Darmasaba"
: "Desa Darmasaba";
// Define page titles based on route
const getPageTitle = () => {
switch (location.pathname) {
case "/":
return "Beranda";
case "/kinerja-divisi":
return "Kinerja Divisi";
case "/pengaduan-layanan-publik":
return "Pengaduan & Layanan Publik";
case "/jenna-analytic":
return "Jenna Analytic";
case "/demografi-pekerjaan":
return "Demografi & Kependudukan";
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";
}
};
return (
<Box
style={{
display: "grid",
gridTemplateColumns: "1fr auto 1fr",
alignItems: "center",
width: "100%",
}}
>
{/* LEFT SPACER (burger sudah di luar) */}
<Box />
<Group justify="space-between" w="100%">
{/* Title */}
<Title order={3} c={"white"}>
{getPageTitle()}
</Title>
{/* CENTER TITLE */}
<Text
c="white"
fw={600}
size="md"
style={{
textAlign: "center",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{title}
</Text>
{/* Right Section */}
<Group gap="md">
{/* User Info */}
<Group gap="sm">
<Box ta="right">
<Text c={"white"} size="sm" fw={500}>
I. B. Surya Prabhawa M...
</Text>
<Text c={"white"} size="xs">
Kepala Desa
</Text>
</Box>
<Avatar color="blue" radius="xl">
<UserIcon color="white" style={{ width: "70%", height: "70%" }} />
</Avatar>
</Group>
{/* RIGHT ICONS */}
<Group gap="xs" justify="flex-end">
<ActionIcon
onClick={toggleColorScheme}
variant="subtle"
radius="xl"
>
{dark ? <Sun size={18} /> : <Moon size={18} />}
</ActionIcon>
{/* Divider */}
<Divider orientation="vertical" h={30} />
<ActionIcon variant="subtle" radius="xl" pos="relative">
<Bell size={18} />
<Badge
size="xs"
color="red"
style={{ position: "absolute", top: -4, right: -4 }}
{/* Icons */}
<Group gap="sm">
<ActionIcon
onClick={() => toggleColorScheme()}
variant="subtle"
size="lg"
radius="xl"
aria-label="Toggle color scheme"
>
10
</Badge>
</ActionIcon>
{dark ? (
<Sun color="white" style={{ width: "70%", height: "70%" }} />
) : (
<Moon color="white" style={{ width: "70%", height: "70%" }} />
)}
</ActionIcon>
<ActionIcon variant="subtle" size="lg" radius="xl" pos="relative">
<Bell color="white" style={{ width: "70%", height: "70%" }} />
<Badge
size="xs"
color="red"
variant="filled"
style={{ position: "absolute", top: 0, right: 0 }}
radius={"xl"}
>
10
</Badge>
</ActionIcon>
<ActionIcon variant="subtle" size="lg" radius="xl">
<IconUserShield
color="white"
style={{ width: "70%", height: "70%" }}
onClick={() => navigate({ to: "/signin" })}
/>
</ActionIcon>
</Group>
</Group>
</Box>
</Group>
);
}

View File

@@ -143,6 +143,13 @@ const HelpPage = () => {
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) => (

View File

@@ -1,537 +1,98 @@
import {
ActionIcon,
Box,
Card,
Divider,
Grid,
GridCol,
Group,
List,
Badge as MantineBadge,
Progress as MantineProgress,
Skeleton,
Stack,
Text,
ThemeIcon,
Title,
useMantineColorScheme,
} from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
Pie,
PieChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { Button } from "@/components/ui/button";
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, fill: "#10B981" },
{ name: "Dikerjakan", value: 8, fill: "#F59E0B" },
{ name: "Segera Dikerjakan", value: 5, fill: "#EF4444" },
{ name: "Dibatalkan", value: 2, fill: "#6B7280" },
];
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
margin={{ top: 20, right: 80, bottom: 20, left: 80 }}
>
<Pie
data={activityProgressStats}
cx="50%"
cy="50%"
labelLine
outerRadius={65}
dataKey="value"
label={({ name, percent }) =>
`${name}: ${percent ? (percent * 100).toFixed(0) : "0"}%`
}
/>
<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>
);
};

View File

@@ -0,0 +1,95 @@
import {
Box,
Card,
Group,
Progress,
Text,
useMantineColorScheme,
} from "@mantine/core";
interface ActivityCardProps {
title: string;
date: string;
progress: number;
status: "selesai" | "berjalan" | "tertunda";
}
export function ActivityCard({
title,
date,
progress,
status,
}: ActivityCardProps) {
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
const getStatusColor = (s: string) => {
switch (s) {
case "selesai":
return "#22C55E";
case "berjalan":
return "#3B82F6";
case "tertunda":
return "#EF4444";
default:
return "#9CA3AF";
}
};
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)",
}}
>
<Box
style={{
borderLeft: `4px solid #3B82F6`,
paddingLeft: 12,
marginBottom: 12,
}}
>
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
{title}
</Text>
</Box>
<Group justify="space-between" mb="xs">
<Text size="xs" c="dimmed">
{date}
</Text>
<Box
style={{
backgroundColor: getStatusColor(status),
color: "white",
padding: "2px 8px",
borderRadius: 4,
fontSize: 11,
fontWeight: 600,
}}
>
{status.toUpperCase()}
</Box>
</Group>
<Progress
value={progress}
size="sm"
radius="xl"
color={progress === 100 ? "green" : "yellow"}
animated={progress < 100}
/>
<Text size="xs" c="dimmed" mt="xs" ta="right">
{progress}%
</Text>
</Card>
);
}

View File

@@ -0,0 +1,41 @@
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",
}}
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,92 @@
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)",
}}
>
<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,69 @@
import { Card, Text, useMantineColorScheme } from "@mantine/core";
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
const documentData = [
{ name: "Gambar", value: 300 },
{ name: "Dokumen", value: 310 },
];
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="value" fill="#3B82F6" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</Card>
);
}

View File

@@ -0,0 +1,65 @@
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)",
}}
>
<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: "#FACC15" },
{ 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>
);
}

View File

@@ -3,6 +3,7 @@ import {
Box,
Collapse,
Group,
Image,
Input,
NavLink as MantineNavLink,
Stack,
@@ -27,35 +28,29 @@ export function Sidebar({ className }: SidebarProps) {
// 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
@@ -66,30 +61,7 @@ export function Sidebar({ className }: SidebarProps) {
return (
<Box className={className}>
{/* Logo */}
<Box
p="md"
style={{ borderBottom: "1px solid var(--mantine-color-gray-3)" }}
>
<Group gap="xs">
<Badge
color="dark"
variant="filled"
size="xl"
radius="md"
py="xs"
px="md"
style={{ fontSize: "1.5rem", fontWeight: "bold" }}
>
DESA
</Badge>
<Badge color="green" variant="filled" size="md" radius="md">
+
</Badge>
</Group>
<Text size="xs" c="dimmed" mt="xs">
Digitalisasi Desa Transparansi Kerja
</Text>
</Box>
<Image src="/logo-desa-plus.png" alt="Logo" />
{/* Search */}
<Box p="md">

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -10,6 +10,18 @@ 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) {
@@ -83,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") {
@@ -146,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
@@ -161,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;
@@ -207,4 +249,3 @@ console.log(
);
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;
},

51
src/routes/bantuan.tsx Normal file
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 HelpPage from "@/components/help-page";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/bantuan")({
component: BantuanPage,
});
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";
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}>
<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,6 +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,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import BumdesPage from "@/components/bumdes-page";
export const Route = createFileRoute("/dashboard/bumdes")({
component: BumdesPage,
});

View File

@@ -1,6 +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,6 +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,6 +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,6 +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,9 @@
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/jenna-analytic")({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/jenna-analytic"!</div>;
}

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

@@ -1,38 +1,35 @@
import {
AppShell,
Burger,
Group,
useMantineColorScheme,
useMantineTheme,
} from "@mantine/core";
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure, useMediaQuery } from "@mantine/hooks";
import { createFileRoute, Outlet, useRouterState } from "@tanstack/react-router";
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("/dashboard")({
component: RouteComponent,
export const Route = createFileRoute("/pengaturan")({
component: PengaturanLayout,
});
function RouteComponent() {
function PengaturanLayout() {
const [opened, { toggle, close }] = useDisclosure();
const { colorScheme } = useMantineColorScheme();
const theme = useMantineTheme();
const routerState = useRouterState();
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)
// Auto close navbar on route change (mobile only)
useEffect(() => {
if (isMobile && opened) {
close();
}
}, [routerState.location.pathname]);
}, [routerState.location.pathname, isMobile, opened, close]);
return (
<AppShell
@@ -45,19 +42,8 @@ function RouteComponent() {
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group
h="100%"
px="lg"
align="center"
wrap="nowrap"
>
<Burger
opened={opened}
onClick={toggle}
hiddenFrom="sm"
size="sm"
/>
<Group h="100%" px="lg" align="center" wrap="nowrap">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header />
</Group>
</AppShell.Header>
@@ -73,7 +59,9 @@ function RouteComponent() {
</AppShell.Navbar>
<AppShell.Main bg={mainBgColor}>
<Outlet />
<div className="p-2">
<Outlet />
</div>
</AppShell.Main>
</AppShell>
);

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import UmumSettings from "@/components/pengaturan/umum";
export const Route = createFileRoute("/dashboard/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

@@ -21,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: {

View File

@@ -21,7 +21,7 @@ export const getEnv = (key: string, defaultValue = ""): string => {
};
export const VITE_PUBLIC_URL = (() => {
// Priority:
// Priority:
// 1. BETTER_AUTH_URL (standard for better-auth)
// 2. VITE_PUBLIC_URL (our app standard)
// 3. window.location.origin (browser fallback)

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