diff --git a/src/frontend/App.tsx b/src/frontend/App.tsx
index 09b029f..838a693 100644
--- a/src/frontend/App.tsx
+++ b/src/frontend/App.tsx
@@ -4,38 +4,38 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRouter, RouterProvider } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
-const theme = createTheme({
+ const theme = createTheme({
primaryColor: 'brand-blue',
colors: {
'brand-blue': [
- '#ebf2ff',
- '#d6e4ff',
- '#adc8ff',
- '#85acff',
- '#5c90ff',
- '#2563eb', // Primary Blue
- '#1e4fb8',
- '#173b85',
- '#102752',
- '#09131f',
+ '#f0f9ff',
+ '#e0f2fe',
+ '#bae6fd',
+ '#7dd3fc',
+ '#38bdf8',
+ '#0ea5e9', // Primary Blue (Sky)
+ '#0284c7',
+ '#0369a1',
+ '#075985',
+ '#0c4a6e',
],
'brand-purple': [
- '#f3ebff',
- '#e7d6ff',
- '#cfadff',
- '#b785ff',
- '#9f5cff',
- '#7c3aed', // Primary Purple
- '#632eb8',
- '#4a2285',
- '#311652',
- '#180b1f',
+ '#faf5ff',
+ '#f3e8ff',
+ '#e9d5ff',
+ '#d8b4fe',
+ '#c084fc',
+ '#a855f7', // Primary Purple
+ '#9333ea',
+ '#7e22ce',
+ '#6b21a8',
+ '#581c87',
],
},
fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif',
headings: {
fontFamily: 'Inter, system-ui, sans-serif',
- fontWeight: '600',
+ fontWeight: '500', // Softer headings
},
})
@@ -59,8 +59,8 @@ declare module '@tanstack/react-router' {
export function App() {
return (
<>
-
-
+
+
diff --git a/src/frontend/components/AppCard.tsx b/src/frontend/components/AppCard.tsx
index 4880e26..3a8d485 100644
--- a/src/frontend/components/AppCard.tsx
+++ b/src/frontend/components/AppCard.tsx
@@ -1,4 +1,4 @@
-import { Card, Group, Text, ThemeIcon, Badge, Avatar, Stack, Button, Progress, Box } from '@mantine/core'
+import { Card, Group, Text, ThemeIcon, Badge, Avatar, Stack, Button, Progress, Box, useComputedColorScheme } from '@mantine/core'
import { Link } from '@tanstack/react-router'
import { TbDeviceMobile, TbActivity, TbAlertTriangle, TbChevronRight } from 'react-icons/tb'
@@ -13,6 +13,7 @@ interface AppCardProps {
export function AppCard({ id, name, status, users, errors, version }: AppCardProps) {
const statusColor = status === 'active' ? 'teal' : status === 'warning' ? 'orange' : 'red'
+ const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
return (
({
root: {
- backgroundColor: 'rgba(30, 41, 59, 0.4)',
- borderColor: 'rgba(255,255,255,0.08)',
- transition: 'transform 0.2s ease, box-shadow 0.2s ease',
+ backgroundColor: 'var(--mantine-color-body)',
+ borderColor: scheme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)',
+ transition: 'transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease, border-color 0.2s ease',
'&:hover': {
transform: 'translateY(-4px)',
- boxShadow: '0 12px 24px -8px rgba(0, 0, 0, 0.4)',
+ boxShadow: theme.shadows.md,
borderColor: 'rgba(37, 99, 235, 0.3)',
},
},
@@ -45,7 +46,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
{name}
- BUILD v{version}
+ VERSION {version}
@@ -53,7 +54,7 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
-
+ {/*
@@ -69,13 +70,13 @@ export function AppCard({ id, name, status, users, errors, version }: AppCardPro
0 ? '#ef4444' : '#64748b'} />
- HEALTH INCIDENTS
+ ERROR
0 ? 'red' : 'dimmed'}>{errors}
-
+ */}
)
diff --git a/src/frontend/components/DashboardLayout.tsx b/src/frontend/components/DashboardLayout.tsx
index 1a119e7..1f59101 100644
--- a/src/frontend/components/DashboardLayout.tsx
+++ b/src/frontend/components/DashboardLayout.tsx
@@ -1,5 +1,6 @@
import { APP_CONFIGS } from '@/frontend/config/appMenus'
import {
+ ActionIcon,
AppShell,
Avatar,
Box,
@@ -14,6 +15,7 @@ import {
ThemeIcon
} from '@mantine/core'
import { useDisclosure } from '@mantine/hooks'
+import { useMantineColorScheme, useComputedColorScheme } from '@mantine/core'
import { Link, useLocation, useMatches, useNavigate, useParams } from '@tanstack/react-router'
import {
TbApps,
@@ -23,7 +25,11 @@ import {
TbDeviceMobile,
TbLogout,
TbSettings,
- TbUserCircle
+ TbUserCircle,
+ TbSun,
+ TbMoon,
+ TbUser,
+ TbHistory
} from 'react-icons/tb'
interface DashboardLayoutProps {
@@ -31,7 +37,10 @@ interface DashboardLayoutProps {
}
export function DashboardLayout({ children }: DashboardLayoutProps) {
- const [opened, { toggle }] = useDisclosure()
+ const [mobileOpened, { toggle: toggleMobile }] = useDisclosure()
+ const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true)
+ const { toggleColorScheme } = useMantineColorScheme()
+ const computedColorScheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
const location = useLocation()
const navigate = useNavigate()
const { appId } = useParams({ strict: false }) as { appId?: string }
@@ -42,7 +51,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
const globalNav = [
{ label: 'Dashboard', icon: TbDashboard, to: '/dashboard' },
{ label: 'Applications', icon: TbApps, to: '/apps' },
- { label: 'Settings', icon: TbSettings, to: '/settings' },
+ { label: 'Log Activity', icon: TbHistory, to: '/logs' },
+ { label: 'Users', icon: TbUser, to: '/users' },
]
const activeApp = appId ? APP_CONFIGS[appId] : null
@@ -54,19 +64,21 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
navbar={{
width: 260,
breakpoint: 'sm',
- collapsed: { mobile: !opened },
+ collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="xl"
styles={(theme) => ({
main: {
- backgroundColor: theme.colors.dark[7], // Dark mode background
+ backgroundColor: computedColorScheme === 'dark' ? theme.colors.dark[9] : theme.colors.gray[0],
+ transition: 'background-color 0.2s ease',
},
})}
>
-
+
+
+ toggleColorScheme()}
+ variant="default"
+ size="lg"
+ aria-label="Toggle color scheme"
+ >
+ {computedColorScheme === 'dark' ? : }
+
LATEST ERROR REPORTS
- }>
+ }>
View All Reports
diff --git a/src/frontend/components/StatsCard.tsx b/src/frontend/components/StatsCard.tsx
index d87b2ee..8525456 100644
--- a/src/frontend/components/StatsCard.tsx
+++ b/src/frontend/components/StatsCard.tsx
@@ -22,8 +22,8 @@ export function StatsCard({ title, value, description, icon: Icon, color, trend
className="premium-card"
styles={(theme) => ({
root: {
- backgroundColor: theme.colors.dark[6],
- borderColor: 'rgba(255,255,255,0.05)',
+ backgroundColor: 'var(--mantine-color-body)',
+ borderColor: 'rgba(128,128,128,0.1)',
},
})}
>
diff --git a/src/frontend/components/SummaryCard.tsx b/src/frontend/components/SummaryCard.tsx
index d3c3681..e1238dc 100644
--- a/src/frontend/components/SummaryCard.tsx
+++ b/src/frontend/components/SummaryCard.tsx
@@ -1,4 +1,4 @@
-import { Card, Group, Text, ThemeIcon, Stack, Progress, Badge } from '@mantine/core'
+import { Card, Group, Text, ThemeIcon, Stack, Progress, Badge, useComputedColorScheme } from '@mantine/core'
import { IconType } from 'react-icons'
import { TbTrendingUp, TbTrendingDown } from 'react-icons/tb'
@@ -27,6 +27,8 @@ export function SummaryCard({
progress,
isError
}: SummaryCardProps) {
+ const scheme = useComputedColorScheme('light', { getInitialValueInEffect: true })
+
return (
({
root: {
- backgroundColor: isError && Number(value) > 0 ? 'rgba(239, 68, 68, 0.05)' : 'rgba(30, 41, 59, 0.4)',
- borderColor: isError && Number(value) > 10 ? 'rgba(239, 68, 68, 0.3)' : 'rgba(255, 255, 255, 0.08)',
- transition: 'transform 0.2s ease',
+ backgroundColor: isError && Number(value) > 0
+ ? (scheme === 'dark' ? 'rgba(239, 68, 68, 0.1)' : 'rgba(255, 241, 242, 1)') // light pink for error in light mode
+ : 'var(--mantine-color-body)',
+ borderColor: isError && Number(value) > 10
+ ? 'rgba(239, 68, 68, 0.3)'
+ : scheme === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.05)',
+ transition: 'transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease',
'&:hover': {
transform: 'translateY(-4px)',
}
diff --git a/src/frontend/config/appMenus.ts b/src/frontend/config/appMenus.ts
index ae77749..2af209b 100644
--- a/src/frontend/config/appMenus.ts
+++ b/src/frontend/config/appMenus.ts
@@ -1,5 +1,5 @@
import { IconType } from 'react-icons'
-import { TbChartBar, TbHistory, TbAlertTriangle, TbSettings, TbShoppingCart, TbPackage, TbCreditCard } from 'react-icons/tb'
+import { TbChartBar, TbHistory, TbAlertTriangle, TbSettings, TbShoppingCart, TbPackage, TbCreditCard, TbBuilding } from 'react-icons/tb'
export interface MenuItem {
value: string
@@ -22,7 +22,7 @@ export const APP_CONFIGS: Record = {
{ value: 'overview', label: 'Overview', icon: TbChartBar, to: '/apps/desa-plus' },
{ value: 'logs', label: 'Log Activity', icon: TbHistory, to: '/apps/desa-plus/logs' },
{ value: 'errors', label: 'Error Reports', icon: TbAlertTriangle, to: '/apps/desa-plus/errors' },
- { value: 'manage', label: 'Manage', icon: TbSettings, to: '/apps/desa-plus/manage' },
+ { value: 'villages', label: 'Villages', icon: TbBuilding, to: '/apps/desa-plus/villages' },
],
},
'e-commerce': {
diff --git a/src/frontend/routes/apps.$appId.index.tsx b/src/frontend/routes/apps.$appId.index.tsx
index a206fd1..47f7525 100644
--- a/src/frontend/routes/apps.$appId.index.tsx
+++ b/src/frontend/routes/apps.$appId.index.tsx
@@ -1,36 +1,22 @@
+import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
+import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
+import { SummaryCard } from '@/frontend/components/SummaryCard'
import {
- Badge,
- Button,
- Card,
+ ActionIcon,
Group,
SimpleGrid,
Stack,
Text,
- Title,
- Paper,
- Box,
- ThemeIcon,
- Select,
- ActionIcon,
- Container,
- Divider,
+ Title
} from '@mantine/core'
-import { createFileRoute, Link, useParams } from '@tanstack/react-router'
-import {
- TbUsers,
- TbActivity,
- TbRefresh,
- TbAlertTriangle,
- TbCalendar,
- TbFilter,
- TbChevronRight,
- TbArrowUpRight,
+import { createFileRoute, useParams } from '@tanstack/react-router'
+import {
+ TbActivity,
+ TbAlertTriangle,
TbBuildingCommunity,
+ TbRefresh,
TbVersions
} from 'react-icons/tb'
-import { SummaryCard } from '@/frontend/components/SummaryCard'
-import { VillageActivityLineChart, VillageComparisonBarChart } from '@/frontend/components/DashboardCharts'
-import { ErrorDataTable } from '@/frontend/components/ErrorDataTable'
export const Route = createFileRoute('/apps/$appId/')({
component: AppOverviewPage,
@@ -43,66 +29,59 @@ function AppOverviewPage() {
return (
{/* 🔝 HEADER SECTION */}
-
-
-
- Overview
-
- }>
- APP: {isDesaPlus ? 'DESA+' : appId.toUpperCase()}
-
- LAST UPDATED: JUST NOW
-
-
+ {/* */}
+
+
+ Overview
+ Last updated: Just now
+
-
-
+ */}
-
+
+ {/* */}
{/* 📊 1. SUMMARY CARDS */}
-
-
-
-
-
+
{appName}
@@ -35,7 +36,9 @@ function AppDetailLayout() {
-
+
+
+
diff --git a/src/frontend/routes/apps.$appId.manage.tsx b/src/frontend/routes/apps.$appId.villages.tsx
similarity index 98%
rename from src/frontend/routes/apps.$appId.manage.tsx
rename to src/frontend/routes/apps.$appId.villages.tsx
index 1dfb05c..ef065c6 100644
--- a/src/frontend/routes/apps.$appId.manage.tsx
+++ b/src/frontend/routes/apps.$appId.villages.tsx
@@ -34,8 +34,8 @@ import {
} from 'react-icons/tb'
import { StatsCard } from '@/frontend/components/StatsCard'
-export const Route = createFileRoute('/apps/$appId/manage')({
- component: AppManagePage,
+export const Route = createFileRoute('/apps/$appId/villages')({
+ component: AppVillagesPage,
})
const mockDevelopers = [
@@ -45,7 +45,7 @@ const mockDevelopers = [
{ value: 'rahmat', label: 'Rahmat Hidayat', avatar: null },
]
-function AppManagePage() {
+function AppVillagesPage() {
const { appId } = useParams({ from: '/apps/$appId' })
const [initModalOpened, { open: openInit, close: closeInit }] = useDisclosure(false)
const [assignModalOpened, { open: openAssign, close: closeAssign }] = useDisclosure(false)
diff --git a/src/frontend/routes/logs.tsx b/src/frontend/routes/logs.tsx
new file mode 100644
index 0000000..4fbc5e6
--- /dev/null
+++ b/src/frontend/routes/logs.tsx
@@ -0,0 +1,214 @@
+import {
+ Badge,
+ Container,
+ Group,
+ Stack,
+ Text,
+ Paper,
+ TextInput,
+ Select,
+ Avatar,
+ Box,
+ Divider,
+} from '@mantine/core'
+import { useState, useMemo } from 'react'
+import { createFileRoute } from '@tanstack/react-router'
+import { TbSearch, TbClock, TbCheck, TbX } from 'react-icons/tb'
+import { DashboardLayout } from '@/frontend/components/DashboardLayout'
+
+export const Route = createFileRoute('/logs')({
+ component: GlobalLogsPage,
+})
+
+const timelineData = [
+ {
+ date: 'TODAY',
+ logs: [
+ { id: 1, time: '12:12 PM', operator: 'Budi Santoso', app: 'Desa+', color: 'blue', content: <>generated document Surat Domisili for Sukatani> },
+ { id: 2, time: '11:42 AM', operator: 'Siti Aminah', app: 'Desa+', color: 'teal', content: <>uploaded financial report Realisasi Q1 for Sukamaju> },
+ { id: 3, time: '10:12 AM', operator: 'System', app: 'Desa+', color: 'red', icon: TbX, content: <>experienced failure in SIAK Sync at }>Cikini>, message: { title: 'Sync Operation Failed (NullPointerException)', text: 'NullPointerException at village_sync.dart:45. The server returned a timeout error while waiting for the master database replica connection. Auto-retry scheduled in 15 minutes.' } },
+ { id: 4, time: '09:42 AM', operator: 'Jane Smith', app: 'E-Commerce', color: 'orange', icon: TbCheck, content: <>resolved payment gateway issue for E-Commerce checkout> },
+ ]
+ },
+ {
+ date: 'YESTERDAY',
+ logs: [
+ { id: 5, time: '05:10 AM', operator: 'System', app: 'System', color: 'cyan', content: <>completed automated Nightly Backup for all 138 villages> },
+ { id: 6, time: '04:50 AM', operator: 'Rahmat Hidayat', app: 'Desa+', color: 'green', content: <>granted Admin access to Desa Bojong Gede operator> },
+ { id: 7, time: '03:42 AM', operator: 'System', app: 'Fitness App', color: 'red', icon: TbX, content: <>detected SocketException across Fitness App wearable sync operations.> },
+ { id: 8, time: '02:33 AM', operator: 'Agus Setiawan', app: 'Desa+', color: 'blue', content: <>verified 145 Surat Kematian entries in batch.> },
+ ]
+ },
+ {
+ date: '12 APRIL, 2026',
+ logs: [
+ { id: 9, time: '03:42 AM', operator: 'Amel', app: 'Desa+', color: 'indigo', content: <>changed version configurations rolling out Desa+ v2.4.1> },
+ { id: 10, time: '02:10 AM', operator: 'John Doe', app: 'E-Commerce', color: 'pink', content: <>updated App setting Require OTP on Login View Details> },
+ ]
+ }
+]
+
+function GlobalLogsPage() {
+ const [search, setSearch] = useState('')
+ const [appFilter, setAppFilter] = useState(null)
+ const [operatorFilter, setOperatorFilter] = useState(null)
+
+ const filteredTimeline = useMemo(() => {
+ return timelineData
+ .map(group => {
+ const filteredLogs = group.logs.filter(log => {
+ if (appFilter && log.app !== appFilter) return false;
+ if (operatorFilter && log.operator !== operatorFilter) return false;
+ if (search) {
+ const lSearch = search.toLowerCase();
+ if (!log.operator.toLowerCase().includes(lSearch) && !log.app.toLowerCase().includes(lSearch)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ return { ...group, logs: filteredLogs };
+ })
+ .filter(group => group.logs.length > 0);
+ }, [search, appFilter, operatorFilter]);
+
+ return (
+
+
+
+ {/* Header Controls */}
+
+ }
+ radius="md"
+ w={220}
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ />
+
+
+
+
+ {/* Timeline Content */}
+
+ {filteredTimeline.length === 0 ? (
+ No logs found matching your filters.
+ ) : filteredTimeline.map((group, groupIndex) => (
+
+ 0 ? "xl" : 0}
+ mb="lg"
+ style={{ textTransform: 'uppercase' }}
+ >
+ {group.date}
+
+
+
+ {group.logs.map((log, logIndex) => {
+ const isLastLog = logIndex === group.logs.length - 1;
+
+ return (
+
+ {/* Left: Time */}
+
+ {log.time}
+
+
+ {/* Middle: Line & Avatar */}
+
+ {/* Vertical Line */}
+ {!isLastLog && (
+
+ )}
+ {/* Avatar */}
+
+ {log.icon ? (
+
+
+
+ ) : (
+
+ {log.operator.charAt(0)}
+
+ )}
+
+
+
+ {/* Right: Content */}
+
+
+ {log.operator}
+ {log.content}
+
+
+ {log.message && (
+
+ {log.message.title}
+
+ {log.message.text}
+
+
+ )}
+
+
+ )
+ })}
+
+
+ {groupIndex < timelineData.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/frontend/routes/settings.tsx b/src/frontend/routes/users.tsx
similarity index 97%
rename from src/frontend/routes/settings.tsx
rename to src/frontend/routes/users.tsx
index c4a3ada..fcbf60a 100644
--- a/src/frontend/routes/settings.tsx
+++ b/src/frontend/routes/users.tsx
@@ -35,8 +35,8 @@ import {
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import { StatsCard } from '@/frontend/components/StatsCard'
-export const Route = createFileRoute('/settings')({
- component: SettingsPage,
+export const Route = createFileRoute('/users')({
+ component: UsersPage,
})
const mockUsers = [
@@ -67,14 +67,14 @@ const roles = [
},
]
-function SettingsPage() {
+function UsersPage() {
return (
- Settings
+ Users
Manage system users, security roles, and application access control.
diff --git a/src/index.css b/src/index.css
index 774be0a..6b5e2f4 100644
--- a/src/index.css
+++ b/src/index.css
@@ -27,8 +27,8 @@ html, body {
height: 100%;
width: 100%;
font-family: var(--font-inter);
- background-color: var(--bg-dark); /* Default to Dark Mode as per App.tsx */
- color: #F8FAFC;
+ /* background-color handled by Mantine */
+ color: var(--mantine-color-text);
}
body {
@@ -53,9 +53,9 @@ body {
/* Premium Dashboard Utilities */
.glass {
- background: rgba(30, 41, 59, 0.7);
+ background: var(--mantine-color-default);
backdrop-filter: blur(12px);
- border: 1px solid rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(128, 128, 128, 0.1);
border-radius: 24px; /* XL rounding for cards */
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}