diff --git a/src/frontend/components/ErrorDataTable.tsx b/src/frontend/components/ErrorDataTable.tsx
index d543e93..ae52464 100644
--- a/src/frontend/components/ErrorDataTable.tsx
+++ b/src/frontend/components/ErrorDataTable.tsx
@@ -149,8 +149,7 @@ export function ErrorDataTable() {
}
styles={{
- header: { padding: '24px', borderBottom: '1px solid rgba(255,255,255,0.1)' },
- content: { background: 'rgba(15, 23, 42, 0.95)', backdropFilter: 'blur(12px)' }
+ header: { padding: '24px', borderBottom: '1px solid var(--mantine-color-default-border)' },
}}
>
{selectedError && (
@@ -175,11 +174,9 @@ export function ErrorDataTable() {
STACK TRACE
-
-
- {selectedError.stackTrace}
-
-
+
+ {selectedError.stackTrace}
+
diff --git a/src/frontend/components/SummaryCard.tsx b/src/frontend/components/SummaryCard.tsx
index e1238dc..fadcd1c 100644
--- a/src/frontend/components/SummaryCard.tsx
+++ b/src/frontend/components/SummaryCard.tsx
@@ -16,6 +16,8 @@ interface SummaryCardProps {
label: string
}
isError?: boolean
+ onClick?: () => void
+ children?: React.ReactNode
}
export function SummaryCard({
@@ -25,7 +27,9 @@ export function SummaryCard({
color = 'brand-blue',
trend,
progress,
- isError
+ isError,
+ onClick,
+ children
}: SummaryCardProps) {
const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
@@ -35,6 +39,8 @@ export function SummaryCard({
padding="xl"
radius="2xl"
className="glass"
+ onClick={onClick}
+ style={{ cursor: onClick ? 'pointer' : 'default' }}
styles={(theme) => ({
root: {
backgroundColor: isError && Number(value) > 0
@@ -95,6 +101,8 @@ export function SummaryCard({
/>
)}
+
+ {children}
)
}
diff --git a/src/frontend/routes/apps.$appId.errors.tsx b/src/frontend/routes/apps.$appId.errors.tsx
index 45c4ec6..071ef1b 100644
--- a/src/frontend/routes/apps.$appId.errors.tsx
+++ b/src/frontend/routes/apps.$appId.errors.tsx
@@ -106,7 +106,7 @@ function AppErrorsPage() {
@@ -153,11 +153,9 @@ function AppErrorsPage() {
STACK TRACE
-
-
- {error.stackTrace}
-
-
+
+ {error.stackTrace}
+
diff --git a/src/frontend/routes/apps.$appId.index.tsx b/src/frontend/routes/apps.$appId.index.tsx
index 47f7525..4112379 100644
--- a/src/frontend/routes/apps.$appId.index.tsx
+++ b/src/frontend/routes/apps.$appId.index.tsx
@@ -7,9 +7,16 @@ import {
SimpleGrid,
Stack,
Text,
- Title
+ Title,
+ Modal,
+ Button,
+ TextInput,
+ Switch,
+ Badge,
+ Textarea
} from '@mantine/core'
-import { createFileRoute, useParams } from '@tanstack/react-router'
+import { useDisclosure } from '@mantine/hooks'
+import { createFileRoute, useParams, useNavigate } from '@tanstack/react-router'
import {
TbActivity,
TbAlertTriangle,
@@ -24,10 +31,28 @@ export const Route = createFileRoute('/apps/$appId/')({
function AppOverviewPage() {
const { appId } = useParams({ from: '/apps/$appId/' })
+ const navigate = useNavigate()
const isDesaPlus = appId === 'desa-plus'
+ const [versionModalOpened, { open: openVersionModal, close: closeVersionModal }] = useDisclosure(false)
return (
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
{/* 🔝 HEADER SECTION */}
{/* */}
@@ -67,7 +92,19 @@ function AppOverviewPage() {
value="v1.2.0"
icon={TbVersions}
color="brand-blue"
- />
+ onClick={openVersionModal}
+ >
+
+
+ Min. Version
+ v1.0.0
+
+
+ Maintenance
+ False
+
+
+
+ onClick={() => navigate({ to: `/apps/${appId}/villages` })}
+ >
+
+ Nonactive Villages
+ 24
+
+
+ >
)
}
diff --git a/src/frontend/routes/apps.$appId.products.tsx b/src/frontend/routes/apps.$appId.products.tsx
index 1e8628e..917c3fb 100644
--- a/src/frontend/routes/apps.$appId.products.tsx
+++ b/src/frontend/routes/apps.$appId.products.tsx
@@ -53,7 +53,7 @@ function ProductsPage() {
{mockProducts.map((product) => (
-
+
@@ -90,7 +90,7 @@ function ProductsPage() {
/>
-
+
diff --git a/src/frontend/routes/apps.$appId.villages.$villageId.tsx b/src/frontend/routes/apps.$appId.villages.$villageId.tsx
new file mode 100644
index 0000000..754a85b
--- /dev/null
+++ b/src/frontend/routes/apps.$appId.villages.$villageId.tsx
@@ -0,0 +1,468 @@
+import { AreaChart } from '@mantine/charts'
+import {
+ Badge,
+ Box,
+ Button,
+ Card,
+ Group,
+ Paper,
+ SegmentedControl,
+ SimpleGrid,
+ Stack,
+ Text,
+ ThemeIcon,
+ Title,
+} from '@mantine/core'
+import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
+import { useState } from 'react'
+import {
+ TbArrowLeft,
+ TbBuildingCommunity,
+ TbCalendar,
+ TbCalendarEvent,
+ TbChartBar,
+ TbCircleCheck,
+ TbEdit,
+ TbHome2,
+ TbLayoutKanban,
+ TbMapPin,
+ TbPower,
+ TbUser,
+ TbUsers,
+ TbUsersGroup,
+ TbWifi
+} from 'react-icons/tb'
+
+export const Route = createFileRoute('/apps/$appId/villages/$villageId')({
+ component: VillageDetailPage,
+})
+
+// ── Mock Data ────────────────────────────────────────────────────────────────
+
+const mockVillages: Record = {
+ 'sukatani': {
+ id: 'sukatani',
+ name: 'Sukatani',
+ kecamatan: 'Tapos',
+ kabupaten: 'Kota Depok',
+ provinsi: 'Jawa Barat',
+ kodePos: '16455',
+ perbekel: 'H. Suryana, S.Sos',
+ createdAt: '2024-03-12',
+ createdBy: 'Admin Pusat',
+ updatedAt: '2024-04-01',
+ status: 'fully integrated',
+ lastSync: '2 menit lalu',
+ stats: { users: 1240, groups: 34, divisions: 8, activities: 4520 },
+ },
+ 'sukamaju': {
+ id: 'sukamaju',
+ name: 'Sukamaju',
+ kecamatan: 'Cilodong',
+ kabupaten: 'Kota Depok',
+ provinsi: 'Jawa Barat',
+ kodePos: '16413',
+ perbekel: 'Drs. H. Mujiono',
+ createdAt: '2024-04-01',
+ createdBy: 'Amel',
+ updatedAt: '2024-04-10',
+ status: 'sync active',
+ lastSync: '15 menit lalu',
+ stats: { users: 980, groups: 28, divisions: 6, activities: 3180 },
+ },
+ 'cikini': {
+ id: 'cikini',
+ name: 'Cikini',
+ kecamatan: 'Menteng',
+ kabupaten: 'Jakarta Pusat',
+ provinsi: 'DKI Jakarta',
+ kodePos: '10330',
+ perbekel: 'Ir. Budi Santoso',
+ createdAt: '2024-05-20',
+ createdBy: 'Jane Smith',
+ updatedAt: '2024-05-25',
+ status: 'sync pending',
+ lastSync: 'Belum pernah sync',
+ stats: { users: 420, groups: 12, divisions: 3, activities: 640 },
+ },
+ 'bojong-gede': {
+ id: 'bojong-gede',
+ name: 'Bojong Gede',
+ kecamatan: 'Bojong Gede',
+ kabupaten: 'Kabupaten Bogor',
+ provinsi: 'Jawa Barat',
+ kodePos: '16920',
+ perbekel: 'H. Rahmat Hidayat, M.Si',
+ createdAt: '2024-02-15',
+ createdBy: 'Rahmat',
+ updatedAt: '2024-04-02',
+ status: 'fully integrated',
+ lastSync: '1 jam lalu',
+ stats: { users: 1890, groups: 51, divisions: 12, activities: 7340 },
+ },
+ 'ciputat': {
+ id: 'ciputat',
+ name: 'Ciputat',
+ kecamatan: 'Ciputat',
+ kabupaten: 'Tangerang Selatan',
+ provinsi: 'Banten',
+ kodePos: '15411',
+ perbekel: 'Drs. Ahmad Fauzi',
+ createdAt: '2024-06-10',
+ createdBy: 'Admin Pusat',
+ updatedAt: '2024-06-15',
+ status: 'sync active',
+ lastSync: '30 menit lalu',
+ stats: { users: 1120, groups: 30, divisions: 7, activities: 3860 },
+ },
+ 'serpong': {
+ id: 'serpong',
+ name: 'Serpong',
+ kecamatan: 'Serpong',
+ kabupaten: 'Tangerang Selatan',
+ provinsi: 'Banten',
+ kodePos: '15310',
+ perbekel: 'H. Bambang Wijaya',
+ createdAt: '2024-07-05',
+ createdBy: 'Amel',
+ updatedAt: '2024-07-10',
+ status: 'sync pending',
+ lastSync: 'Belum tersinkronisasi',
+ stats: { users: 280, groups: 8, divisions: 2, activities: 310 },
+ },
+}
+
+// ── Chart Data Generators ─────────────────────────────────────────────────────
+
+function generateDailyData() {
+ const days = ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min']
+ const today = new Date()
+ return Array.from({ length: 14 }, (_, i) => {
+ const d = new Date(today)
+ d.setDate(today.getDate() - (13 - i))
+ const dayName = days[d.getDay() === 0 ? 6 : d.getDay() - 1]
+ const dateStr = `${dayName} ${d.getDate()}/${d.getMonth() + 1}`
+ return {
+ label: dateStr,
+ aktivitas: Math.floor(Math.random() * 300 + 60),
+ }
+ })
+}
+
+function generateMonthlyData() {
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agt', 'Sep', 'Okt', 'Nov', 'Des']
+ return months.map((m) => ({
+ label: m,
+ aktivitas: Math.floor(Math.random() * 2000 + 800),
+ }))
+}
+
+function generateYearlyData() {
+ return ['2021', '2022', '2023', '2024'].map((y) => ({
+ label: y,
+ aktivitas: Math.floor(Math.random() * 15000 + 5000),
+ }))
+}
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+
+const statusConfig = {
+ 'fully integrated': { color: 'teal', label: 'Terintegrasi Penuh' },
+ 'sync active': { color: 'blue', label: 'Sync Aktif' },
+ 'sync pending': { color: 'orange', label: 'Menunggu Sync' },
+}
+
+function formatDate(dateStr: string) {
+ return new Date(dateStr).toLocaleDateString('id-ID', {
+ day: 'numeric', month: 'long', year: 'numeric',
+ })
+}
+
+// ── Activity Chart ────────────────────────────────────────────────────────────
+
+type ChartPeriod = 'daily' | 'monthly' | 'yearly'
+
+function ActivityChart() {
+ const [period, setPeriod] = useState('monthly')
+
+ const dataMap: Record = {
+ daily: generateDailyData(),
+ monthly: generateMonthlyData(),
+ yearly: generateYearlyData(),
+ }
+
+ const labels: Record = {
+ daily: 'Harian (14 hari terakhir)',
+ monthly: 'Bulanan (tahun ini)',
+ yearly: 'Tahunan',
+ }
+
+ const data = dataMap[period]
+
+ return (
+
+
+
+
+
+
+
+ Log Aktivitas Desa
+ {labels[period]}
+
+
+
+ setPeriod(v as ChartPeriod)}
+ size="xs"
+ radius="md"
+ data={[
+ { value: 'daily', label: 'Harian' },
+ { value: 'monthly', label: 'Bulanan' },
+ { value: 'yearly', label: 'Tahunan' },
+ ]}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+// ── Main Page ─────────────────────────────────────────────────────────────────
+
+function VillageDetailPage() {
+ const { appId, villageId } = useParams({ from: '/apps/$appId/villages/$villageId' })
+ const navigate = useNavigate()
+ const village = mockVillages[villageId]
+
+ const goBack = () => navigate({ to: '/apps/$appId/villages', params: { appId } })
+
+ if (!village) {
+ return (
+
+
+ Desa tidak ditemukan
+ ID desa "{villageId}" tidak terdaftar dalam sistem.
+ } onClick={goBack}>
+ Kembali ke Daftar
+
+
+ )
+ }
+
+ const cfg = statusConfig[village.status as keyof typeof statusConfig]
+ const { stats } = village
+
+ return (
+
+
+ {/* ── Back Button ── */}
+
+ }
+ radius="md"
+ onClick={goBack}
+ >
+ Daftar Desa
+
+
+ {/* Action Buttons */}
+
+ : }
+ onClick={() => alert(`Toggle status for ${village.name}`)}
+ radius="md"
+ >
+ {village.status === 'fully integrated' || village.status === 'sync active' ? 'Nonaktifkan Desa' : 'Aktifkan Desa'}
+
+ }
+ onClick={() => alert(`Edit settings for ${village.name}`)}
+ radius="md"
+ >
+ Edit Desa
+
+
+
+
+ {/* ── Header Banner ── */}
+
+ {/* Decorative blobs */}
+
+
+
+
+
+
+
+
+
+
+ {village.name}
+
+
+
+
+ Kec. {village.kecamatan} · {village.kabupaten} · {village.provinsi}
+
+
+
+
+
+
+ Perbekel: {village.perbekel}
+
+
+
+
+ }
+ >
+ {cfg.label}
+
+
+ Kode Pos: {village.kodePos}
+
+
+
+
+
+ {/* Last Sync block */}
+
+ Last Sync
+
+
+ {village.lastSync}
+
+
+
+
+
+ {/* ── Stats Cards ── */}
+
+ {[
+ { icon: TbUsers, label: 'Jumlah User', value: stats.users.toLocaleString('id-ID'), color: 'blue' },
+ { icon: TbUsersGroup, label: 'Jumlah Grup', value: stats.groups.toLocaleString('id-ID'), color: 'violet' },
+ { icon: TbLayoutKanban, label: 'Jumlah Divisi', value: stats.divisions.toLocaleString('id-ID'), color: 'teal' },
+ { icon: TbCalendarEvent, label: 'Jumlah Kegiatan', value: stats.activities.toLocaleString('id-ID'), color: 'orange' },
+ ].map((s) => (
+
+
+
+
+
+ {s.label}
+
+ {s.value}
+
+ ))}
+
+
+ {/* ── Chart + Info Panels ── */}
+
+ {/* Left (3/4): Activity Chart */}
+
+
+ {/* Right (1/4): Informasi Sistem */}
+
+
+
+
+
+ Informasi Sistem
+
+
+ {[
+ { label: 'Tanggal Dibuat', value: formatDate(village.createdAt) },
+ { label: 'Dibuat Oleh', value: village.createdBy },
+ { label: 'Terakhir Diperbarui', value: formatDate(village.updatedAt) },
+ ].map((item, idx, arr) => (
+
+ {item.label}
+ {item.value}
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/src/frontend/routes/apps.$appId.villages.index.tsx b/src/frontend/routes/apps.$appId.villages.index.tsx
new file mode 100644
index 0000000..0bd6a08
--- /dev/null
+++ b/src/frontend/routes/apps.$appId.villages.index.tsx
@@ -0,0 +1,366 @@
+import { useState } from 'react'
+import {
+ Badge,
+ Container,
+ Group,
+ Stack,
+ Text,
+ Title,
+ Paper,
+ Button,
+ ActionIcon,
+ TextInput,
+ Tooltip,
+ SimpleGrid,
+ Avatar,
+ Box,
+ SegmentedControl,
+ Card,
+ Divider,
+ ThemeIcon,
+} from '@mantine/core'
+import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
+import {
+ TbPlus,
+ TbSearch,
+ TbBuildingCommunity,
+ TbLayoutGrid,
+ TbList,
+ TbMapPin,
+ TbCalendar,
+ TbUser,
+ TbHome2,
+ TbArrowRight,
+ TbChevronRight,
+} from 'react-icons/tb'
+
+export const Route = createFileRoute('/apps/$appId/villages/')({
+ component: AppVillagesIndexPage,
+})
+
+const mockVillages = [
+ {
+ id: 'sukatani',
+ name: 'Sukatani',
+ kecamatan: 'Tapos',
+ kabupaten: 'Kota Depok',
+ provinsi: 'Jawa Barat',
+ perbekel: 'H. Suryana, S.Sos',
+ createdAt: '2024-03-12',
+ createdBy: 'Admin Pusat',
+ status: 'fully integrated',
+ population: 4500,
+ },
+ {
+ id: 'sukamaju',
+ name: 'Sukamaju',
+ kecamatan: 'Cilodong',
+ kabupaten: 'Kota Depok',
+ provinsi: 'Jawa Barat',
+ perbekel: 'Drs. H. Mujiono',
+ createdAt: '2024-04-01',
+ createdBy: 'Amel',
+ status: 'sync active',
+ population: 3800,
+ },
+ {
+ id: 'cikini',
+ name: 'Cikini',
+ kecamatan: 'Menteng',
+ kabupaten: 'Jakarta Pusat',
+ provinsi: 'DKI Jakarta',
+ perbekel: 'Ir. Budi Santoso',
+ createdAt: '2024-05-20',
+ createdBy: 'Jane Smith',
+ status: 'sync pending',
+ population: 2100,
+ },
+ {
+ id: 'bojong-gede',
+ name: 'Bojong Gede',
+ kecamatan: 'Bojong Gede',
+ kabupaten: 'Kabupaten Bogor',
+ provinsi: 'Jawa Barat',
+ perbekel: 'H. Rahmat Hidayat, M.Si',
+ createdAt: '2024-02-15',
+ createdBy: 'Rahmat',
+ status: 'fully integrated',
+ population: 6700,
+ },
+ {
+ id: 'ciputat',
+ name: 'Ciputat',
+ kecamatan: 'Ciputat',
+ kabupaten: 'Tangerang Selatan',
+ provinsi: 'Banten',
+ perbekel: 'Drs. Ahmad Fauzi',
+ createdAt: '2024-06-10',
+ createdBy: 'Admin Pusat',
+ status: 'sync active',
+ population: 5200,
+ },
+ {
+ id: 'serpong',
+ name: 'Serpong',
+ kecamatan: 'Serpong',
+ kabupaten: 'Tangerang Selatan',
+ provinsi: 'Banten',
+ perbekel: 'H. Bambang Wijaya',
+ createdAt: '2024-07-05',
+ createdBy: 'Amel',
+ status: 'sync pending',
+ population: 8900,
+ },
+]
+
+const statusConfig = {
+ 'fully integrated': { color: 'teal', label: 'Terintegrasi' },
+ 'sync active': { color: 'blue', label: 'Sync Aktif' },
+ 'sync pending': { color: 'orange', label: 'Sync Pending' },
+}
+
+function formatDate(dateStr: string) {
+ return new Date(dateStr).toLocaleDateString('id-ID', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ })
+}
+
+function VillageGridCard({ village, onClick }: { village: typeof mockVillages[0]; onClick: () => void }) {
+ const cfg = statusConfig[village.status as keyof typeof statusConfig]
+ return (
+
+
+
+
+
+
+ {cfg.label}
+
+
+
+
+ {village.name}
+
+
+
+
+ Kec. {village.kecamatan} · {village.kabupaten}
+
+
+
+
+ {village.provinsi}
+
+
+
+
+
+
+
+ Perbekel:
+ {village.perbekel}
+
+
+
+ Dibuat:
+ {formatDate(village.createdAt)}
+
+
+
+ Oleh:
+ {village.createdBy}
+
+
+
+ }
+ styles={{ root: { fontSize: 12 } }}
+ >
+ Lihat Detail
+
+
+ )
+}
+
+function VillageListRow({ village, onClick }: { village: typeof mockVillages[0]; onClick: () => void }) {
+ const cfg = statusConfig[village.status as keyof typeof statusConfig]
+ return (
+
+
+
+
+
+
+
+
+ {village.name}
+
+ {cfg.label}
+
+
+
+
+
+ Kec. {village.kecamatan} · {village.kabupaten} · {village.provinsi}
+
+
+
+
+
+
+
+ Perbekel
+ {village.perbekel}
+
+
+ Dibuat
+ {formatDate(village.createdAt)}
+
+
+ Oleh
+
+
+ {village.createdBy}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function AppVillagesIndexPage() {
+ const { appId } = useParams({ from: '/apps/$appId' })
+ const navigate = useNavigate()
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
+ const [search, setSearch] = useState('')
+
+ const isDesaPlus = appId === 'desa-plus'
+
+ const filtered = mockVillages.filter((v) =>
+ [v.name, v.kecamatan, v.kabupaten, v.provinsi, v.perbekel]
+ .join(' ')
+ .toLowerCase()
+ .includes(search.toLowerCase())
+ )
+
+ const handleVillageClick = (villageId: string) => {
+ navigate({ to: '/apps/$appId/villages/$villageId', params: { appId, villageId } })
+ }
+
+ if (!isDesaPlus) {
+ return (
+
+
+
+ General Management
+ This feature is currently customized for Desa+. Other apps coming soon.
+
+
+ )
+ }
+
+ return (
+
+
+
+ Daftar Desa
+
+ {filtered.length} desa terdaftar dalam platform Desa+
+
+
+ }
+ radius="md"
+ >
+ Tambah Desa
+
+
+
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ radius="md"
+ style={{ flex: 1, maxWidth: 400 }}
+ />
+ setViewMode(v as 'grid' | 'list')}
+ data={[
+ { value: 'grid', label: },
+ { value: 'list', label: },
+ ]}
+ radius="md"
+ />
+
+
+ {filtered.length === 0 ? (
+
+
+ Tidak ada desa yang cocok dengan pencarian.
+
+ ) : viewMode === 'grid' ? (
+
+ {filtered.map((village) => (
+ handleVillageClick(village.id)}
+ />
+ ))}
+
+ ) : (
+
+ {filtered.map((village) => (
+ handleVillageClick(village.id)}
+ />
+ ))}
+
+ )}
+
+ )
+}
diff --git a/src/frontend/routes/apps.$appId.villages.tsx b/src/frontend/routes/apps.$appId.villages.tsx
index ef065c6..62f1e40 100644
--- a/src/frontend/routes/apps.$appId.villages.tsx
+++ b/src/frontend/routes/apps.$appId.villages.tsx
@@ -1,278 +1,9 @@
-import { useState } from 'react'
-import {
- Badge,
- Container,
- Group,
- Stack,
- Text,
- Title,
- Paper,
- Table,
- Button,
- ActionIcon,
- TextInput,
- Select,
- Tooltip,
- SimpleGrid,
- Modal,
- Avatar,
- Box,
- NumberInput,
-} from '@mantine/core'
-import { useDisclosure } from '@mantine/hooks'
-import { createFileRoute, useParams } from '@tanstack/react-router'
-import {
- TbPlus,
- TbSearch,
- TbPencil,
- TbTrash,
- TbUserPlus,
- TbCircleCheck,
- TbRefresh,
- TbUser,
- TbBuildingCommunity,
-} from 'react-icons/tb'
-import { StatsCard } from '@/frontend/components/StatsCard'
+import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/apps/$appId/villages')({
- component: AppVillagesPage,
+ component: VillagesLayout,
})
-const mockDevelopers = [
- { value: 'john-doe', label: 'John Doe', avatar: null },
- { value: 'amel', label: 'Amel', avatar: null },
- { value: 'jane-smith', label: 'Jane Smith', avatar: null },
- { value: 'rahmat', label: 'Rahmat Hidayat', avatar: null },
-]
-
-function AppVillagesPage() {
- const { appId } = useParams({ from: '/apps/$appId' })
- const [initModalOpened, { open: openInit, close: closeInit }] = useDisclosure(false)
- const [assignModalOpened, { open: openAssign, close: closeAssign }] = useDisclosure(false)
- const [selectedVillage, setSelectedVillage] = useState(null)
-
- const isDesaPlus = appId === 'desa-plus'
-
- const mockVillages = [
- { id: 1, name: 'Sukatani', kecamatan: 'Tapos', population: 4500, status: 'fully integrated', developer: 'John Doe', lastUpdate: '2 mins ago' },
- { id: 2, name: 'Sukamaju', kecamatan: 'Cilodong', population: 3800, status: 'sync active', developer: 'Amel', lastUpdate: '15 mins ago' },
- { id: 3, name: 'Cikini', kecamatan: 'Menteng', population: 2100, status: 'sync pending', developer: 'Jane Smith', lastUpdate: '-' },
- { id: 4, name: 'Bojong Gede', kecamatan: 'Bojong Gede', population: 6700, status: 'fully integrated', developer: 'Rahmat', lastUpdate: '1 hour ago' },
- ]
-
- if (!isDesaPlus) {
- return (
-
-
-
- General Management
- This feature is currently customized for Desa+. Other apps coming soon.
-
-
- )
- }
-
- return (
-
- {/* Metrics Row */}
-
-
-
-
-
-
-
-
-
- Village Deployment Center
- Monitor and configure **Desa+** village instances across all districts.
-
- }
- radius="md"
- onClick={openInit}
- >
- Initialize New Village
-
-
-
-
-
- }
- style={{ flex: 1 }}
- radius="md"
- />
-
-
-
-
-
- Village Profile
- District
- Integration Status
- Lead Developer
- Last Sync
- Actions
-
-
-
- {mockVillages.map((village) => (
-
-
-
- {village.name}
- {village.population.toLocaleString()} Residents
-
-
-
- {village.kecamatan}
-
-
- }
- radius="sm"
- style={{ textTransform: 'uppercase', fontVariant: 'small-caps' }}
- >
- {village.status}
-
-
-
-
-
- {village.developer}
- { setSelectedVillage(village); openAssign(); }}
- >
-
-
-
-
-
-
- {village.lastUpdate}
-
-
-
-
- {village.status === 'sync pending' && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
- {/* MODALS */}
- Desa+ Instance Initialization}
- radius="xl"
- centered
- padding="xl"
- >
-
-
-
-
-
-
-
-
-
-
- INITIAL SYNC MODULES
-
- PENDUDUK
- KEUANGAN
- PELAYANAN
- APBDes
-
-
-
-
-
-
-
-
-
- Assign Lead Developer}
- radius="xl"
- centered
- padding="xl"
- >
-
- Assign a dedicated reviewer for {selectedVillage?.name} instance stability.
- }
- radius="md"
- searchable
- />
-
-
-
-
-
-
-
- )
+function VillagesLayout() {
+ return
}
diff --git a/src/index.css b/src/index.css
index 6b5e2f4..02ab2e7 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1,5 +1,5 @@
@import '@mantine/core/styles.css';
-
+@import '@mantine/charts/styles.css';
:root {
--font-inter: 'Inter', system-ui, -apple-system, sans-serif;
@@ -111,3 +111,40 @@ body {
.data-table tbody tr:hover {
background: rgba(124, 58, 237, 0.03);
}
+
+/* Village Cards */
+.village-card {
+ transition: var(--transition-smooth);
+ background: var(--mantine-color-body);
+ border-color: rgba(128, 128, 128, 0.12) !important;
+}
+.village-card:hover {
+ transform: translateY(-6px);
+ box-shadow: 0 16px 32px -12px rgba(37, 99, 235, 0.25);
+ border-color: rgba(37, 99, 235, 0.3) !important;
+}
+
+.village-list-row {
+ transition: var(--transition-smooth);
+ background: var(--mantine-color-body);
+ border-color: rgba(128, 128, 128, 0.12) !important;
+}
+.village-list-row:hover {
+ transform: translateX(4px);
+ box-shadow: 0 4px 16px -6px rgba(37, 99, 235, 0.2);
+ border-color: rgba(37, 99, 235, 0.3) !important;
+}
+
+/* Village Detail Page Grid */
+.village-detail-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ align-items: start;
+}
+
+@media (min-width: 768px) {
+ .village-detail-grid {
+ grid-template-columns: 3fr 1fr;
+ }
+}