Compare commits
4 Commits
nico/13-ma
...
nico/17-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 158a2db435 | |||
| 2d68d4dc06 | |||
| 97e6caa332 | |||
| f0c37272b9 |
139
Kinerja-Divisi-New.md
Normal file
139
Kinerja-Divisi-New.md
Normal file
@@ -0,0 +1,139 @@
|
||||
Create a modern admin dashboard UI for a village management system using React 19 + Vite + TailwindCSS + Mantine components + Recharts.
|
||||
|
||||
Design style:
|
||||
- Clean, soft UI with rounded corners (2xl)
|
||||
- Light gray background (#f5f6f8)
|
||||
- Card-based layout with subtle shadow
|
||||
- Smooth spacing and consistent padding
|
||||
- Professional government-style but still modern
|
||||
- Use Inter or system font
|
||||
- Primary color: dark blue
|
||||
- Accent color: orange for progress
|
||||
- Success color: green
|
||||
- Use Mantine components where possible
|
||||
|
||||
Layout:
|
||||
- Responsive grid layout (desktop-first)
|
||||
- 4 summary cards on top (horizontal)
|
||||
- 2 columns main content below
|
||||
- Left sidebar for division list
|
||||
- Right content for charts and activity
|
||||
|
||||
---
|
||||
|
||||
## 🔹 TOP CARDS (4 ITEMS)
|
||||
Each card contains:
|
||||
- Title (e.g: "Rakor 2025")
|
||||
- Progress bar (orange)
|
||||
- Date (small text)
|
||||
- Status badge "Selesai" (green)
|
||||
|
||||
Use:
|
||||
- Mantine Card
|
||||
- Mantine Progress
|
||||
- Mantine Badge
|
||||
|
||||
---
|
||||
|
||||
## 🔹 LEFT PANEL - "Divisi teraktif"
|
||||
Vertical list of divisions:
|
||||
- Each item is clickable
|
||||
- Show division name + number of activities
|
||||
- Rounded container with hover effect
|
||||
- Chevron icon on right
|
||||
|
||||
Example items:
|
||||
- Kesejahteraan (37 kegiatan)
|
||||
- Pemerintahan (26 kegiatan)
|
||||
- Keuangan (17 kegiatan)
|
||||
- etc
|
||||
|
||||
Use:
|
||||
- Scrollable container
|
||||
- Soft border + hover highlight
|
||||
|
||||
---
|
||||
|
||||
## 🔹 CENTER - BAR CHART (Jumlah Dokumen)
|
||||
- Use Recharts
|
||||
- Two bars:
|
||||
- Gambar
|
||||
- Dokumen
|
||||
- Color:
|
||||
- Yellow/orange
|
||||
- Green
|
||||
- Show Y axis scale (0–400)
|
||||
|
||||
---
|
||||
|
||||
## 🔹 RIGHT - PIE CHART (Progres Kegiatan)
|
||||
- Use Recharts PieChart
|
||||
- Segments:
|
||||
- Selesai (green ~83%)
|
||||
- Dikerjakan (orange ~16%)
|
||||
- Segera dikerjakan (blue)
|
||||
- Dibatalkan (red)
|
||||
- Include legend below
|
||||
|
||||
---
|
||||
|
||||
## 🔹 BOTTOM LEFT - Diskusi Panel
|
||||
- List of discussion messages
|
||||
- Each item:
|
||||
- Title
|
||||
- Sender name
|
||||
- Date
|
||||
- Styled like notification cards
|
||||
- Compact and clean
|
||||
|
||||
---
|
||||
|
||||
## 🔹 BOTTOM RIGHT - "Acara Hari Ini"
|
||||
- Empty state
|
||||
- Show text: "Tidak ada acara hari ini"
|
||||
- Centered, muted text
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ TECH REQUIREMENTS
|
||||
|
||||
- Use React functional components
|
||||
- Use TanStack Router (file-based or route config)
|
||||
- Use Mantine for UI components
|
||||
- Use Tailwind for layout and spacing
|
||||
- Use Recharts for charts
|
||||
- State management: Valtio (simple global state)
|
||||
- Date formatting: dayjs
|
||||
- Icons: lucide-react
|
||||
|
||||
---
|
||||
|
||||
## 📁 COMPONENT STRUCTURE
|
||||
|
||||
- components/
|
||||
- DashboardCard.tsx
|
||||
- DivisionList.tsx
|
||||
- BarChartCard.tsx
|
||||
- PieChartCard.tsx
|
||||
- DiscussionList.tsx
|
||||
- EmptyState.tsx
|
||||
|
||||
- routes/
|
||||
- dashboard.tsx
|
||||
|
||||
---
|
||||
|
||||
## ✨ EXTRA (IMPORTANT FOR VIBE CODING)
|
||||
|
||||
- Add subtle hover animations (scale 1.02)
|
||||
- Smooth transitions (150–200ms)
|
||||
- Keep spacing consistent (gap-4 / gap-6)
|
||||
- Avoid clutter, prioritize readability
|
||||
- Make it feel "calm and productive"
|
||||
|
||||
---
|
||||
|
||||
Output:
|
||||
- Full React component code (modular, not monolithic)
|
||||
- Clean, readable, production-ready
|
||||
- No unnecessary comments
|
||||
168
Pengaduan-New.md
Normal file
168
Pengaduan-New.md
Normal file
@@ -0,0 +1,168 @@
|
||||
Create a modern analytics dashboard UI for a village complaint system (Pengaduan Dashboard).
|
||||
|
||||
Tech stack:
|
||||
- React 19 + Vite (Bun runtime)
|
||||
- Mantine UI (core components)
|
||||
- TailwindCSS (layout & spacing only)
|
||||
- Recharts (charts)
|
||||
- TanStack Router
|
||||
- Icons: lucide-react
|
||||
- State: Valtio
|
||||
- Date: dayjs
|
||||
|
||||
---
|
||||
|
||||
## 🎨 DESIGN STYLE
|
||||
|
||||
- Clean, minimal, and soft dashboard
|
||||
- Background: light gray (#f3f4f6)
|
||||
- Card: white with subtle shadow
|
||||
- Border radius: 16px–24px (rounded-2xl)
|
||||
- Typography: medium contrast (not too bold)
|
||||
- Primary color: navy blue (#1E3A5F)
|
||||
- Accent: soft blue + neutral gray
|
||||
- Icons inside circular solid background
|
||||
|
||||
Spacing:
|
||||
- Use gap-6 consistently
|
||||
- Internal padding: p-5 or p-6
|
||||
- Layout must feel breathable (no clutter)
|
||||
|
||||
---
|
||||
|
||||
## 🧱 LAYOUT STRUCTURE
|
||||
|
||||
### 🔹 TOP SECTION (4 STAT CARDS - GRID)
|
||||
Grid: 4 columns (responsive → 2 / 1)
|
||||
|
||||
Each card contains:
|
||||
- Title (small, muted)
|
||||
- Big number (bold, large)
|
||||
- Subtitle (small gray text)
|
||||
- Right side: circular icon container
|
||||
|
||||
Example:
|
||||
- Total Pengaduan → 42 → "Bulan ini"
|
||||
- Baru → 14 → "Belum diproses"
|
||||
- Diproses → 14 → "Sedang ditangani"
|
||||
- Selesai → 14 → "Terselesaikan"
|
||||
|
||||
Use:
|
||||
- Mantine Card
|
||||
- Group justify="space-between"
|
||||
- Icon inside circle (bg navy, icon white)
|
||||
|
||||
---
|
||||
|
||||
## 📈 MAIN CHART (FULL WIDTH)
|
||||
Title: "Tren Pengaduan"
|
||||
|
||||
- Use Recharts LineChart
|
||||
- Smooth line (monotone)
|
||||
- Show dots on each point
|
||||
- Data: Apr → Okt
|
||||
- Value range: 30–60
|
||||
|
||||
Style:
|
||||
- Minimal grid (light dashed)
|
||||
- No heavy colors (use gray/blue line)
|
||||
- Rounded container card
|
||||
- Add small top-right icon (expand)
|
||||
|
||||
---
|
||||
|
||||
## 📊 BOTTOM SECTION (3 COLUMN GRID)
|
||||
|
||||
### 🔹 LEFT: "Surat Terbanyak"
|
||||
- Horizontal bar chart (Recharts)
|
||||
- Categories:
|
||||
- KTP
|
||||
- KK
|
||||
- Domisili
|
||||
- Usaha
|
||||
- Lainnya
|
||||
|
||||
Style:
|
||||
- Dark blue bars
|
||||
- Rounded edges
|
||||
- Clean axis
|
||||
|
||||
---
|
||||
|
||||
### 🔹 CENTER: "Pengajuan Terbaru"
|
||||
List of activity cards:
|
||||
|
||||
Each item:
|
||||
- Name (bold)
|
||||
- Subtitle (jenis surat)
|
||||
- Time (small text)
|
||||
- Status badge (kanan)
|
||||
|
||||
Status:
|
||||
- baru → red
|
||||
- proses → blue
|
||||
- selesai → green
|
||||
|
||||
Style:
|
||||
- Card per item
|
||||
- Soft border
|
||||
- Rounded
|
||||
- Compact spacing
|
||||
|
||||
---
|
||||
|
||||
### 🔹 RIGHT: "Ajuan Ide Inovatif"
|
||||
List mirip dengan pengajuan terbaru:
|
||||
|
||||
Each item:
|
||||
- Nama
|
||||
- Judul ide
|
||||
- Waktu
|
||||
- Button kecil "Detail"
|
||||
|
||||
Style:
|
||||
- Right-aligned action button
|
||||
- Light border
|
||||
- Clean spacing
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ COMPONENT STRUCTURE
|
||||
|
||||
components/
|
||||
- StatCard.tsx
|
||||
- LineChartCard.tsx
|
||||
- BarChartCard.tsx
|
||||
- ActivityList.tsx
|
||||
- IdeaList.tsx
|
||||
|
||||
routes/
|
||||
- dashboard.tsx
|
||||
|
||||
---
|
||||
|
||||
## ✨ INTERACTIONS (IMPORTANT)
|
||||
|
||||
- Hover card → scale(1.02)
|
||||
- Transition: 150ms ease
|
||||
- Icon circle slightly pop on hover
|
||||
- List item hover → subtle bg change
|
||||
|
||||
---
|
||||
|
||||
## 🎯 UX DETAILS
|
||||
|
||||
- Numbers must be visually dominant
|
||||
- Icons must balance layout (not too big)
|
||||
- Avoid heavy borders
|
||||
- Keep everything aligned perfectly
|
||||
- No clutter
|
||||
|
||||
---
|
||||
|
||||
## 🚀 OUTPUT
|
||||
|
||||
- Modular React components (NOT one file)
|
||||
- Clean code (production-ready)
|
||||
- Use Mantine properly (no hacky inline styles unless needed)
|
||||
- Use Tailwind only for layout/grid/spacing
|
||||
302
PromptDashboard.md
Normal file
302
PromptDashboard.md
Normal file
@@ -0,0 +1,302 @@
|
||||
Buat halaman dashboard admin modern untuk sistem pemerintahan desa bernama **Darmasaba Dashboard NOC**.
|
||||
|
||||
Gunakan stack berikut:
|
||||
|
||||
Frontend:
|
||||
|
||||
* React 19
|
||||
* Bun runtime
|
||||
* Vite
|
||||
* TailwindCSS
|
||||
* Mantine UI
|
||||
* Mantine Charts atau Recharts
|
||||
* Tabler Icons
|
||||
* TanStack Router
|
||||
* Dayjs
|
||||
|
||||
UI harus modular dengan reusable components.
|
||||
|
||||
Gunakan **TailwindCSS sebagai styling utama** dengan warna dari konfigurasi berikut:
|
||||
|
||||
Primary:
|
||||
darmasaba-navy (#1E3A5F)
|
||||
|
||||
Secondary:
|
||||
darmasaba-blue (#3B82F6)
|
||||
|
||||
Success:
|
||||
#22C55E
|
||||
|
||||
Warning:
|
||||
#FACC15
|
||||
|
||||
Danger:
|
||||
#EF4444
|
||||
|
||||
Background:
|
||||
#F5F8FB
|
||||
|
||||
Dashboard harus memiliki **Light Mode dan Dark Mode**.
|
||||
|
||||
Dark Mode Color Rules:
|
||||
background: #0F172A
|
||||
card: #1E293B
|
||||
border: #334155
|
||||
text: #E2E8F0
|
||||
|
||||
Card style:
|
||||
|
||||
* rounded-xl
|
||||
* soft shadow
|
||||
* padding besar
|
||||
* border subtle
|
||||
* smooth hover animation
|
||||
|
||||
Gunakan grid layout responsive.
|
||||
|
||||
---
|
||||
|
||||
SECTION 1 — PROGRAM KEGIATAN
|
||||
|
||||
Buat 4 card horizontal di bagian atas yang menampilkan kegiatan desa.
|
||||
|
||||
Setiap card memiliki:
|
||||
|
||||
* header biru
|
||||
* progress bar kegiatan
|
||||
* tanggal kegiatan
|
||||
* badge status
|
||||
|
||||
Data card:
|
||||
|
||||
1.
|
||||
|
||||
Judul: Rakor 2025
|
||||
Tanggal: 3 Juli 2025
|
||||
Progress: 90%
|
||||
Status: selesai
|
||||
|
||||
2.
|
||||
|
||||
Judul: Pemutakhiran Indeks Desa
|
||||
Tanggal: 3 Juli 2025
|
||||
Progress: 85%
|
||||
Status: selesai
|
||||
|
||||
3.
|
||||
|
||||
Judul: Mengurus Akta Cerai Warga
|
||||
Tanggal: 3 Juli 2025
|
||||
Progress: 80%
|
||||
Status: selesai
|
||||
|
||||
4.
|
||||
|
||||
Judul: Pasek 7 Desa Adat
|
||||
Tanggal: 3 Juli 2025
|
||||
Progress: 92%
|
||||
Status: selesai
|
||||
|
||||
Progress bar:
|
||||
|
||||
* rounded
|
||||
* warna warning
|
||||
* animasi smooth
|
||||
|
||||
Status badge:
|
||||
|
||||
* success color
|
||||
|
||||
---
|
||||
|
||||
SECTION 2 — GRID DASHBOARD
|
||||
|
||||
Layout:
|
||||
|
||||
3 column grid.
|
||||
|
||||
Left column (sidebar style):
|
||||
Divisi Teraktif
|
||||
|
||||
List item card dengan arrow icon.
|
||||
|
||||
Data:
|
||||
|
||||
Kesejahteraan — 37 kegiatan
|
||||
Pemerintahan — 26 kegiatan
|
||||
Keuangan — 17 kegiatan
|
||||
Sekretaris Desa — 15 kegiatan
|
||||
Tata Usaha TK — 14 kegiatan
|
||||
Perangkat Kewilayahan — 12 kegiatan
|
||||
Pelayanan — 10 kegiatan
|
||||
Perencanaan — 9 kegiatan
|
||||
Tata Usaha & Umum — 7 kegiatan
|
||||
|
||||
Setiap item:
|
||||
|
||||
* rounded
|
||||
* hover effect
|
||||
* arrow icon kanan
|
||||
|
||||
---
|
||||
|
||||
Middle column:
|
||||
|
||||
Jumlah Dokumen
|
||||
|
||||
Gunakan **Bar Chart**.
|
||||
|
||||
Kategori:
|
||||
|
||||
* Gambar
|
||||
* Dokumen
|
||||
|
||||
Nilai:
|
||||
|
||||
* Gambar: 300
|
||||
* Dokumen: 310
|
||||
|
||||
Gunakan:
|
||||
Recharts atau Mantine Charts.
|
||||
|
||||
---
|
||||
|
||||
Right column:
|
||||
|
||||
Progres Kegiatan
|
||||
|
||||
Gunakan **Pie Chart**.
|
||||
|
||||
Data:
|
||||
|
||||
Selesai — 83.33%
|
||||
Dikerjakan — 16.67%
|
||||
Segera Dikerjakan — 0%
|
||||
Dibatalkan — 0%
|
||||
|
||||
Legend harus berwarna.
|
||||
|
||||
---
|
||||
|
||||
SECTION 3 — DISCUSSION PANEL
|
||||
|
||||
Judul: Diskusi
|
||||
|
||||
Tampilkan list diskusi internal staf.
|
||||
|
||||
Item card memiliki:
|
||||
|
||||
* icon chat
|
||||
* judul pesan
|
||||
* nama pengirim
|
||||
* tanggal
|
||||
|
||||
Contoh data:
|
||||
|
||||
"Kepada Pelayanan, mohon di cek..."
|
||||
Pengirim: I.B Surya Prabhawa Manu
|
||||
Tanggal: 12 Apr 2025
|
||||
|
||||
"Kepada staf perencanaan @suar..."
|
||||
Pengirim: Ni Nyoman Yuliani
|
||||
Tanggal: 14 Jun 2025
|
||||
|
||||
"ijin atau mohon kepada KBD sar..."
|
||||
Pengirim: Ni Wayan Martini
|
||||
Tanggal: 12 Apr 2025
|
||||
|
||||
---
|
||||
|
||||
SECTION 4 — ACARA HARI INI
|
||||
|
||||
Card sederhana.
|
||||
|
||||
Jika tidak ada acara tampilkan:
|
||||
|
||||
"Tidak ada acara hari ini"
|
||||
|
||||
---
|
||||
|
||||
SECTION 5 — ARSIP DIGITAL PERANGKAT DESA
|
||||
|
||||
Grid 2 column.
|
||||
|
||||
Menu arsip:
|
||||
|
||||
Surat Keputusan
|
||||
Dokumentasi
|
||||
Laporan Keuangan
|
||||
Notulensi Rapat
|
||||
|
||||
Setiap item berupa card clickable dengan:
|
||||
|
||||
* icon dokumen
|
||||
* border
|
||||
* hover effect
|
||||
|
||||
---
|
||||
|
||||
DESIGN STYLE
|
||||
|
||||
Gunakan gaya:
|
||||
|
||||
Modern Government Dashboard
|
||||
Clean UI
|
||||
Soft shadow
|
||||
Rounded-xl
|
||||
Spacing besar
|
||||
Minimalistic
|
||||
|
||||
---
|
||||
|
||||
RESPONSIVE RULES
|
||||
|
||||
Desktop:
|
||||
12 column grid
|
||||
|
||||
Tablet:
|
||||
6 column grid
|
||||
|
||||
Mobile:
|
||||
single column stack
|
||||
|
||||
---
|
||||
|
||||
COMPONENT STRUCTURE
|
||||
|
||||
src/components/dashboard
|
||||
|
||||
activity-card.tsx
|
||||
division-list.tsx
|
||||
document-chart.tsx
|
||||
progress-chart.tsx
|
||||
discussion-panel.tsx
|
||||
event-card.tsx
|
||||
archive-card.tsx
|
||||
|
||||
src/pages
|
||||
|
||||
dashboard.tsx
|
||||
|
||||
---
|
||||
|
||||
CODE QUALITY
|
||||
|
||||
Gunakan:
|
||||
|
||||
* React hooks
|
||||
* reusable components
|
||||
* Mantine components jika perlu
|
||||
* Tailwind utility classes
|
||||
* dark mode support
|
||||
* responsive layout
|
||||
* clean TypeScript
|
||||
|
||||
---
|
||||
|
||||
Output:
|
||||
|
||||
* Halaman dashboard lengkap
|
||||
* Semua komponen reusable
|
||||
* Chart sudah bekerja
|
||||
* Layout identik dengan desain dashboard modern pemerintahan
|
||||
@@ -386,4 +386,4 @@ const BumdesPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BumdesPage;
|
||||
export default BumdesPage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Grid, Stack, useMantineColorScheme } from "@mantine/core";
|
||||
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";
|
||||
@@ -8,149 +8,26 @@ import { SatisfactionChart } from "./dashboard/satisfaction-chart";
|
||||
import { SDGSCard } from "./dashboard/sdgs-card";
|
||||
import { StatCard } from "./dashboard/stat-card";
|
||||
|
||||
// SDGs Icons
|
||||
function EnergyIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24 4L14 24H22L20 44L34 20H26L24 4Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PeaceIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="24" cy="24" r="20" stroke="currentColor" strokeWidth="2" />
|
||||
<path
|
||||
d="M24 4V44M24 24L10 38M24 24L38 38"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function HealthIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M24 44C24 44 6 28 6 18C6 11.373 11.373 6 18 6C21.5 6 24.5 7.5 24 12C23.5 7.5 26.5 6 30 6C36.627 6 42 11.373 42 18C42 28 24 44 24 44Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PovertyIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="6" y="18" width="36" height="26" rx="2" fill="currentColor" />
|
||||
<path
|
||||
d="M14 18V12C14 8.686 16.686 6 20 6H28C31.314 6 34 8.686 34 12V18"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function OceanIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6 30C6 30 10 26 14 30C18 34 22 30 26 30C30 30 34 34 38 30C42 26 46 30 46 30"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d="M6 38C6 38 10 34 14 38C18 42 22 38 26 38C30 38 34 42 38 38C42 34 46 38 46 38"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<circle cx="24" cy="16" r="6" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const sdgsData = [
|
||||
{
|
||||
title: "Desa Berenergi Bersih dan Terbarukan",
|
||||
score: 99.64,
|
||||
icon: <EnergyIcon />,
|
||||
color: "#FACC15",
|
||||
bgColor: "#FEF9C3",
|
||||
image: "SDGS-7.png",
|
||||
},
|
||||
{
|
||||
title: "Desa Damai Berkeadilan",
|
||||
score: 78.65,
|
||||
icon: <PeaceIcon />,
|
||||
color: "#3B82F6",
|
||||
bgColor: "#DBEAFE",
|
||||
image: "SDGS-16.png",
|
||||
},
|
||||
{
|
||||
title: "Desa Sehat dan Sejahtera",
|
||||
score: 77.37,
|
||||
icon: <HealthIcon />,
|
||||
color: "#22C55E",
|
||||
bgColor: "#DCFCE7",
|
||||
image: "SDGS-3.png",
|
||||
},
|
||||
{
|
||||
title: "Desa Tanpa Kemiskinan",
|
||||
score: 52.62,
|
||||
icon: <PovertyIcon />,
|
||||
color: "#EF4444",
|
||||
bgColor: "#FEE2E2",
|
||||
},
|
||||
{
|
||||
title: "Desa Peduli Lingkungan Laut",
|
||||
score: 50.0,
|
||||
icon: <OceanIcon />,
|
||||
color: "#06B6D4",
|
||||
bgColor: "#CFFAFE",
|
||||
image: "SDGS-1.png",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -202,37 +79,35 @@ export function DashboardContent() {
|
||||
|
||||
{/* Section 2: Chart & Division Progress */}
|
||||
<Grid gutter="lg">
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||
<ChartSurat />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<DivisionProgress />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Section 3: APBDes Chart */}
|
||||
<ChartAPBDes />
|
||||
|
||||
{/* Section 4 & 5: Activity List & Satisfaction Chart */}
|
||||
<Grid gutter="lg">
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<ActivityList />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, lg: 6 }}>
|
||||
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||
<SatisfactionChart />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{/* Section 3: APBDes Chart */}
|
||||
<Grid gutter="lg">
|
||||
<Grid.Col span={{ base: 12, lg: 7 }}>
|
||||
<DivisionProgress />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, lg: 5 }}>
|
||||
<ActivityList />
|
||||
{/* <SatisfactionChart /> */}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<ChartAPBDes />
|
||||
|
||||
{/* Section 6: SDGs Desa Cards */}
|
||||
<Grid gutter="md">
|
||||
{sdgsData.map((sdg, index) => (
|
||||
<Grid.Col key={index} span={{ base: 12, sm: 6, md: 4, lg: 2.4 }}>
|
||||
<Grid.Col key={index} span={{ base: 9, md: 3 }}>
|
||||
<SDGSCard
|
||||
image={<Image src={sdg.image} alt={sdg.title} />}
|
||||
title={sdg.title}
|
||||
score={sdg.score}
|
||||
icon={sdg.icon}
|
||||
color={sdg.color}
|
||||
bgColor={sdg.bgColor}
|
||||
/>
|
||||
</Grid.Col>
|
||||
))}
|
||||
|
||||
@@ -36,6 +36,7 @@ export function ActivityList() {
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group gap="xs" mb="lg">
|
||||
<Calendar
|
||||
|
||||
@@ -39,6 +39,7 @@ export function ChartAPBDes() {
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||
Grafik APBDes
|
||||
|
||||
@@ -42,6 +42,7 @@ export function ChartSurat() {
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Box>
|
||||
|
||||
@@ -39,6 +39,7 @@ export function DivisionProgress() {
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb="lg">
|
||||
Divisi Teraktif
|
||||
|
||||
@@ -32,6 +32,7 @@ export function SatisfactionChart() {
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Title order={4} c={dark ? "white" : "gray.9"} mb={5}>
|
||||
Tingkat Kepuasan
|
||||
|
||||
@@ -4,18 +4,10 @@ import type { ReactNode } from "react";
|
||||
interface SDGSCardProps {
|
||||
title: string;
|
||||
score: number;
|
||||
icon: ReactNode;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
image: ReactNode;
|
||||
}
|
||||
|
||||
export function SDGSCard({
|
||||
title,
|
||||
score,
|
||||
icon,
|
||||
color,
|
||||
bgColor,
|
||||
}: SDGSCardProps) {
|
||||
export function SDGSCard({ title, score, image }: SDGSCardProps) {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
@@ -24,29 +16,28 @@ export function SDGSCard({
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={bgColor}
|
||||
style={{
|
||||
borderColor: dark ? "#334155" : bgColor,
|
||||
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 size="sm" c={dark ? "white" : "gray.8"} fw={500} mb="xs">
|
||||
<Text
|
||||
ta={"center"}
|
||||
size="sm"
|
||||
c={dark ? "white" : "gray.8"}
|
||||
fw={500}
|
||||
mb="xs"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="xl" fw={700} c={color}>
|
||||
<Text ta={"center"} size="xl" c={dark ? "white" : "gray.8"} fw={700}>
|
||||
{score.toFixed(2)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
style={{
|
||||
color,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Box>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -408,4 +408,4 @@ const DemografiPekerjaan = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DemografiPekerjaan;
|
||||
export default DemografiPekerjaan;
|
||||
|
||||
@@ -118,4 +118,4 @@ export function Header() {
|
||||
</Group>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,4 +432,4 @@ const HelpPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default HelpPage;
|
||||
export default HelpPage;
|
||||
|
||||
@@ -280,4 +280,4 @@ const JennaAnalytic = () => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default JennaAnalytic;
|
||||
export default JennaAnalytic;
|
||||
|
||||
@@ -322,4 +322,4 @@ const KeamananPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default KeamananPage;
|
||||
export default KeamananPage;
|
||||
|
||||
@@ -354,4 +354,4 @@ const KeuanganAnggaran = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default KeuanganAnggaran;
|
||||
export default KeuanganAnggaran;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Grid, Stack } from "@mantine/core";
|
||||
import {
|
||||
ActivityCard,
|
||||
ArchiveCard,
|
||||
DiscussionPanel,
|
||||
DivisionList,
|
||||
DocumentChart,
|
||||
EventCard,
|
||||
ProgressChart,
|
||||
} from ".";
|
||||
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 = [
|
||||
@@ -15,25 +14,25 @@ const programKegiatanData = [
|
||||
title: "Rakor 2025",
|
||||
date: "3 Juli 2025",
|
||||
progress: 90,
|
||||
status: "selesai" as const,
|
||||
status: "Selesai" as const,
|
||||
},
|
||||
{
|
||||
title: "Pemutakhiran Indeks Desa",
|
||||
date: "3 Juli 2025",
|
||||
progress: 85,
|
||||
status: "selesai" as const,
|
||||
status: "Selesai" as const,
|
||||
},
|
||||
{
|
||||
title: "Mengurus Akta Cerai Warga",
|
||||
date: "3 Juli 2025",
|
||||
progress: 80,
|
||||
status: "selesai" as const,
|
||||
status: "Selesai" as const,
|
||||
},
|
||||
{
|
||||
title: "Pasek 7 Desa Adat",
|
||||
date: "3 Juli 2025",
|
||||
progress: 92,
|
||||
status: "selesai" as const,
|
||||
status: "Selesai" as const,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Progress,
|
||||
Text,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { Card, Text, Progress, Group, Box } from "@mantine/core";
|
||||
|
||||
interface ActivityCardProps {
|
||||
title: string;
|
||||
date: string;
|
||||
progress: number;
|
||||
status: "selesai" | "berjalan" | "tertunda";
|
||||
status: "Selesai" | "Berjalan" | "Tertunda";
|
||||
}
|
||||
|
||||
export function ActivityCard({
|
||||
@@ -20,16 +13,13 @@ export function ActivityCard({
|
||||
progress,
|
||||
status,
|
||||
}: ActivityCardProps) {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const dark = colorScheme === "dark";
|
||||
|
||||
const getStatusColor = (s: string) => {
|
||||
switch (s) {
|
||||
case "selesai":
|
||||
const getStatusColor = () => {
|
||||
switch (status) {
|
||||
case "Selesai":
|
||||
return "#22C55E";
|
||||
case "berjalan":
|
||||
case "Berjalan":
|
||||
return "#3B82F6";
|
||||
case "tertunda":
|
||||
case "Tertunda":
|
||||
return "#EF4444";
|
||||
default:
|
||||
return "#9CA3AF";
|
||||
@@ -38,58 +28,62 @@ export function ActivityCard({
|
||||
|
||||
return (
|
||||
<Card
|
||||
p="md"
|
||||
radius="xl"
|
||||
withBorder
|
||||
bg={dark ? "#1E293B" : "white"}
|
||||
p={0}
|
||||
withBorder={false}
|
||||
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)",
|
||||
backgroundColor: "#F3F4F6",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 🔵 HEADER */}
|
||||
<Box
|
||||
style={{
|
||||
borderLeft: `4px solid #3B82F6`,
|
||||
paddingLeft: 12,
|
||||
marginBottom: 12,
|
||||
backgroundColor: "#1E3A5F",
|
||||
padding: "16px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Text size="sm" fw={600} c={dark ? "white" : "#1E3A5F"}>
|
||||
<Text c="white" fw={700} size="md">
|
||||
{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,
|
||||
{/* CONTENT */}
|
||||
<Box p="md">
|
||||
{/* PROGRESS */}
|
||||
<Progress
|
||||
value={progress}
|
||||
radius="xl"
|
||||
size="lg"
|
||||
color="orange"
|
||||
styles={{
|
||||
root: {
|
||||
height: 16,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{status.toUpperCase()}
|
||||
</Box>
|
||||
</Group>
|
||||
/>
|
||||
|
||||
<Progress
|
||||
value={progress}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
color={progress === 100 ? "green" : "yellow"}
|
||||
animated={progress < 100}
|
||||
/>
|
||||
{/* FOOTER */}
|
||||
<Group justify="space-between" mt="md">
|
||||
<Text size="sm" fw={500}>
|
||||
{date}
|
||||
</Text>
|
||||
|
||||
<Text size="xs" c="dimmed" mt="xs" ta="right">
|
||||
{progress}%
|
||||
</Text>
|
||||
<Box
|
||||
style={{
|
||||
backgroundColor: getStatusColor(),
|
||||
color: "white",
|
||||
padding: "4px 12px",
|
||||
borderRadius: 999,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</Box>
|
||||
</Group>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export function ArchiveCard({ item, onClick }: ArchiveCardProps) {
|
||||
cursor: "pointer",
|
||||
transition: "transform 0.2s, box-shadow 0.2s",
|
||||
}}
|
||||
h="100%"
|
||||
onClick={onClick}
|
||||
>
|
||||
<Group gap="md">
|
||||
|
||||
@@ -48,6 +48,7 @@ export function DiscussionPanel() {
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group gap="xs" mb="md">
|
||||
<MessageCircle size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
@@ -10,8 +11,8 @@ import {
|
||||
} from "recharts";
|
||||
|
||||
const documentData = [
|
||||
{ name: "Gambar", value: 300 },
|
||||
{ name: "Dokumen", value: 310 },
|
||||
{ name: "Gambar", jumlah: 300, color: "#FACC15" },
|
||||
{ name: "Dokumen", jumlah: 310, color: "#22C55E" },
|
||||
];
|
||||
|
||||
export function DocumentChart() {
|
||||
@@ -61,7 +62,11 @@ export function DocumentChart() {
|
||||
}}
|
||||
labelStyle={{ color: dark ? "#E2E8F0" : "#374151" }}
|
||||
/>
|
||||
<Bar dataKey="value" fill="#3B82F6" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="jumlah" radius={[4, 4, 0, 0]}>
|
||||
{documentData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
|
||||
@@ -33,6 +33,7 @@ export function EventCard({ agendas = [] }: EventCardProps) {
|
||||
? "0 1px 3px 0 rgb(0 0 0 / 0.1)"
|
||||
: "0 1px 3px 0 rgb(0 0 0 / 0.1)",
|
||||
}}
|
||||
h="100%"
|
||||
>
|
||||
<Group gap="xs" mb="md">
|
||||
<Calendar size={20} color={dark ? "#E2E8F0" : "#1E3A5F"} />
|
||||
|
||||
@@ -10,7 +10,7 @@ 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: "Dikerjakan", value: 16.67, color: "#F59E0B" },
|
||||
{ name: "Segera Dikerjakan", value: 0, color: "#3B82F6" },
|
||||
{ name: "Dibatalkan", value: 0, color: "#EF4444" },
|
||||
];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import {
|
||||
Box,
|
||||
Collapse,
|
||||
Group,
|
||||
Image,
|
||||
Input,
|
||||
NavLink as MantineNavLink,
|
||||
Stack,
|
||||
@@ -60,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">
|
||||
@@ -204,4 +182,4 @@ export function Sidebar({ className }: SidebarProps) {
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,4 +462,4 @@ const SosialPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SosialPage;
|
||||
export default SosialPage;
|
||||
|
||||
@@ -1 +1 @@
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
|
||||
410
src/index.ts
410
src/index.ts
@@ -12,240 +12,240 @@ 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
|
||||
}
|
||||
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) {
|
||||
// Development: Use Vite middleware
|
||||
const { createVite } = await import("./vite");
|
||||
const vite = await createVite();
|
||||
// Development: Use Vite middleware
|
||||
const { createVite } = await import("./vite");
|
||||
const vite = await createVite();
|
||||
|
||||
// Serve PWA/TWA assets in dev (root and nested path support)
|
||||
const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath);
|
||||
// Serve PWA/TWA assets in dev (root and nested path support)
|
||||
const _servePwaAsset = (srcPath: string) => () => Bun.file(srcPath);
|
||||
|
||||
app.post("/__open-in-editor", ({ body }) => {
|
||||
const { relativePath, lineNumber, columnNumber } = body as {
|
||||
relativePath: string;
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
};
|
||||
app.post("/__open-in-editor", ({ body }) => {
|
||||
const { relativePath, lineNumber, columnNumber } = body as {
|
||||
relativePath: string;
|
||||
lineNumber: number;
|
||||
columnNumber: number;
|
||||
};
|
||||
|
||||
openInEditor(relativePath, {
|
||||
line: lineNumber,
|
||||
column: columnNumber,
|
||||
editor: "antigravity",
|
||||
});
|
||||
openInEditor(relativePath, {
|
||||
line: lineNumber,
|
||||
column: columnNumber,
|
||||
editor: "antigravity",
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
// Vite middleware for other requests
|
||||
app.all("*", async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
// Vite middleware for other requests
|
||||
app.all("*", async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Serve transformed index.html for root or any path that should be handled by the SPA
|
||||
if (
|
||||
pathname === "/" ||
|
||||
(!pathname.includes(".") &&
|
||||
!pathname.startsWith("/@") &&
|
||||
!pathname.startsWith("/inspector") &&
|
||||
!pathname.startsWith("/__open-stack-frame-in-editor") &&
|
||||
!pathname.startsWith("/api"))
|
||||
) {
|
||||
try {
|
||||
const htmlPath = path.resolve("src/index.html");
|
||||
let html = fs.readFileSync(htmlPath, "utf-8");
|
||||
html = await vite.transformIndexHtml(pathname, html);
|
||||
// Serve transformed index.html for root or any path that should be handled by the SPA
|
||||
if (
|
||||
pathname === "/" ||
|
||||
(!pathname.includes(".") &&
|
||||
!pathname.startsWith("/@") &&
|
||||
!pathname.startsWith("/inspector") &&
|
||||
!pathname.startsWith("/__open-stack-frame-in-editor") &&
|
||||
!pathname.startsWith("/api"))
|
||||
) {
|
||||
try {
|
||||
const htmlPath = path.resolve("src/index.html");
|
||||
let html = fs.readFileSync(htmlPath, "utf-8");
|
||||
html = await vite.transformIndexHtml(pathname, html);
|
||||
|
||||
return new Response(html, {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return new Response(html, {
|
||||
headers: { "Content-Type": "text/html" },
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise<Response>((resolve) => {
|
||||
// Use a Proxy to mock Node.js req because Bun's Request is read-only
|
||||
const req = new Proxy(request, {
|
||||
get(target, prop) {
|
||||
if (prop === "url") return pathname + url.search;
|
||||
if (prop === "method") return request.method;
|
||||
if (prop === "headers")
|
||||
return Object.fromEntries(request.headers as any);
|
||||
return (target as any)[prop];
|
||||
},
|
||||
}) as any;
|
||||
return new Promise<Response>((resolve) => {
|
||||
// Use a Proxy to mock Node.js req because Bun's Request is read-only
|
||||
const req = new Proxy(request, {
|
||||
get(target, prop) {
|
||||
if (prop === "url") return pathname + url.search;
|
||||
if (prop === "method") return request.method;
|
||||
if (prop === "headers")
|
||||
return Object.fromEntries(request.headers as any);
|
||||
return (target as any)[prop];
|
||||
},
|
||||
}) as any;
|
||||
|
||||
const res = {
|
||||
statusCode: 200,
|
||||
setHeader(name: string, value: string) {
|
||||
this.headers[name.toLowerCase()] = value;
|
||||
},
|
||||
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") {
|
||||
body = data;
|
||||
} else if (data) {
|
||||
body = String(data);
|
||||
}
|
||||
const res = {
|
||||
statusCode: 200,
|
||||
setHeader(name: string, value: string) {
|
||||
this.headers[name.toLowerCase()] = value;
|
||||
},
|
||||
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") {
|
||||
body = data;
|
||||
} else if (data) {
|
||||
body = String(data);
|
||||
}
|
||||
|
||||
resolve(
|
||||
new Response(body || "", {
|
||||
status: this.statusCode,
|
||||
headers: this.headers,
|
||||
}),
|
||||
);
|
||||
},
|
||||
// Minimal event emitter mock
|
||||
once() {
|
||||
return this;
|
||||
},
|
||||
on() {
|
||||
return this;
|
||||
},
|
||||
emit() {
|
||||
return this;
|
||||
},
|
||||
removeListener() {
|
||||
return this;
|
||||
},
|
||||
} as any;
|
||||
resolve(
|
||||
new Response(body || "", {
|
||||
status: this.statusCode,
|
||||
headers: this.headers,
|
||||
}),
|
||||
);
|
||||
},
|
||||
// Minimal event emitter mock
|
||||
once() {
|
||||
return this;
|
||||
},
|
||||
on() {
|
||||
return this;
|
||||
},
|
||||
emit() {
|
||||
return this;
|
||||
},
|
||||
removeListener() {
|
||||
return this;
|
||||
},
|
||||
} as any;
|
||||
|
||||
vite.middlewares(req, res, (err: any) => {
|
||||
if (err) {
|
||||
console.error("Vite middleware error:", err);
|
||||
resolve(new Response(err.stack || err.toString(), { status: 500 }));
|
||||
return;
|
||||
}
|
||||
// If Vite doesn't handle it, return 404
|
||||
resolve(new Response("Not Found", { status: 404 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
vite.middlewares(req, res, (err: any) => {
|
||||
if (err) {
|
||||
console.error("Vite middleware error:", err);
|
||||
resolve(new Response(err.stack || err.toString(), { status: 500 }));
|
||||
return;
|
||||
}
|
||||
// If Vite doesn't handle it, return 404
|
||||
resolve(new Response("Not Found", { status: 404 }));
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Production: Final catch-all for static files and SPA fallback
|
||||
app.all("*", async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
// Production: Final catch-all for static files and SPA fallback
|
||||
app.all("*", async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// 1. Try exact match in dist
|
||||
let filePath = path.join(
|
||||
"dist",
|
||||
pathname === "/" ? "index.html" : pathname,
|
||||
);
|
||||
// 1. Try exact match in dist
|
||||
let filePath = path.join(
|
||||
"dist",
|
||||
pathname === "/" ? "index.html" : pathname,
|
||||
);
|
||||
|
||||
// 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build)
|
||||
if (isProduction) {
|
||||
const srcPath = path.join("src", pathname);
|
||||
if (fs.existsSync(srcPath)) {
|
||||
filePath = srcPath;
|
||||
}
|
||||
// Check public folder for static assets
|
||||
const publicPath = path.join("public", pathname);
|
||||
if (fs.existsSync(publicPath)) {
|
||||
filePath = publicPath;
|
||||
}
|
||||
}
|
||||
// 1.1 Special handling for PWA/TWA assets that might not be in dist (since we use custom bun build)
|
||||
if (isProduction) {
|
||||
const srcPath = path.join("src", pathname);
|
||||
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
|
||||
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||
if (pathname.includes(".") && !pathname.endsWith("/")) {
|
||||
const filename = path.basename(pathname);
|
||||
// 2. If not found and looks like an asset (has extension), try root of dist or src
|
||||
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
||||
if (pathname.includes(".") && !pathname.endsWith("/")) {
|
||||
const filename = path.basename(pathname);
|
||||
|
||||
// Try root of dist
|
||||
const fallbackDistPath = path.join("dist", filename);
|
||||
if (
|
||||
fs.existsSync(fallbackDistPath) &&
|
||||
fs.statSync(fallbackDistPath).isFile()
|
||||
) {
|
||||
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
|
||||
if (pathname.includes("assetlinks.json")) {
|
||||
const srcFilename = pathname.includes("assetlinks.json")
|
||||
? ".well-known/assetlinks.json"
|
||||
: filename;
|
||||
const fallbackSrcPath = path.join("src", srcFilename);
|
||||
if (
|
||||
fs.existsSync(fallbackSrcPath) &&
|
||||
fs.statSync(fallbackSrcPath).isFile()
|
||||
) {
|
||||
filePath = fallbackSrcPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try root of dist
|
||||
const fallbackDistPath = path.join("dist", filename);
|
||||
if (
|
||||
fs.existsSync(fallbackDistPath) &&
|
||||
fs.statSync(fallbackDistPath).isFile()
|
||||
) {
|
||||
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
|
||||
if (pathname.includes("assetlinks.json")) {
|
||||
const srcFilename = pathname.includes("assetlinks.json")
|
||||
? ".well-known/assetlinks.json"
|
||||
: filename;
|
||||
const fallbackSrcPath = path.join("src", srcFilename);
|
||||
if (
|
||||
fs.existsSync(fallbackSrcPath) &&
|
||||
fs.statSync(fallbackSrcPath).isFile()
|
||||
) {
|
||||
filePath = fallbackSrcPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||
const file = Bun.file(filePath);
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
Vary: "Accept-Encoding",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
||||
const file = Bun.file(filePath);
|
||||
return new Response(file, {
|
||||
headers: {
|
||||
Vary: "Accept-Encoding",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 3. SPA Fallback: Serve index.html
|
||||
const indexHtml = path.join("dist", "index.html");
|
||||
if (fs.existsSync(indexHtml)) {
|
||||
return new Response(Bun.file(indexHtml), {
|
||||
headers: {
|
||||
Vary: "Accept-Encoding",
|
||||
},
|
||||
});
|
||||
}
|
||||
// 3. SPA Fallback: Serve index.html
|
||||
const indexHtml = path.join("dist", "index.html");
|
||||
if (fs.existsSync(indexHtml)) {
|
||||
return new Response(Bun.file(indexHtml), {
|
||||
headers: {
|
||||
Vary: "Accept-Encoding",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Response("Not Found", { status: 404 });
|
||||
});
|
||||
return new Response("Not Found", { status: 404 });
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(PORT);
|
||||
|
||||
console.log(
|
||||
`🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`,
|
||||
`🚀 Server running at http://localhost:${PORT} in ${isProduction ? "production" : "development"} mode`,
|
||||
);
|
||||
|
||||
export type ApiApp = typeof app;
|
||||
|
||||
@@ -152,4 +152,4 @@ export function createProtectedRoute(options: ProtectedRouteOptions = {}) {
|
||||
* Default Middleware Export
|
||||
* ================================ */
|
||||
|
||||
export const protectedRouteMiddleware = createProtectedRoute();
|
||||
export const protectedRouteMiddleware = createProtectedRoute();
|
||||
|
||||
@@ -28,4 +28,4 @@ export const Route = createRootRoute({
|
||||
|
||||
function RootComponent() {
|
||||
return <Outlet />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,4 +48,4 @@ function DashboardPage() {
|
||||
</AppShell.Main>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {
|
||||
AppShell,
|
||||
Burger,
|
||||
Group,
|
||||
useMantineColorScheme,
|
||||
} 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";
|
||||
@@ -44,12 +43,7 @@ function PengaturanLayout() {
|
||||
>
|
||||
<AppShell.Header bg={headerBgColor}>
|
||||
<Group h="100%" px="lg" align="center" wrap="nowrap">
|
||||
<Burger
|
||||
opened={opened}
|
||||
onClick={toggle}
|
||||
hiddenFrom="sm"
|
||||
size="sm"
|
||||
/>
|
||||
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
|
||||
<Header />
|
||||
</Group>
|
||||
</AppShell.Header>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user