upd: tampilan

This commit is contained in:
2026-04-02 10:30:21 +08:00
parent 39d659acd0
commit 47d26799ad
28 changed files with 2701 additions and 237 deletions

View File

@@ -1,20 +1,23 @@
import { useQuery } from '@tanstack/react-query'
import {
Avatar,
Badge,
Button,
Card,
Container,
Group,
Paper,
SimpleGrid,
Stack,
Text,
ThemeIcon,
Title,
Paper,
Table,
Loader,
} from '@mantine/core'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { TbChartBar, TbLogout, TbSettings, TbUsers } from 'react-icons/tb'
import { createFileRoute, redirect, Link } from '@tanstack/react-router'
import { TbActivity, TbApps, TbMessageReport, TbUsers, TbChevronRight } from 'react-icons/tb'
import { useLogout, useSession } from '@/frontend/hooks/useAuth'
import { DashboardLayout } from '@/frontend/components/DashboardLayout'
import { StatsCard } from '@/frontend/components/StatsCard'
import { AppCard } from '@/frontend/components/AppCard'
export const Route = createFileRoute('/dashboard')({
beforeLoad: async ({ context }) => {
@@ -33,62 +36,140 @@ export const Route = createFileRoute('/dashboard')({
component: DashboardPage,
})
const stats = [
{ title: 'Users', value: '1,234', icon: TbUsers, color: 'blue' },
{ title: 'Revenue', value: '$12.4k', icon: TbChartBar, color: 'green' },
{ title: 'Settings', value: '3 active', icon: TbSettings, color: 'violet' },
const recentErrors = [
{ id: 1, app: 'Desa+', message: 'NullPointerException at village_sync.dart:45', version: '2.4.1', time: '2 mins ago', severity: 'critical' },
{ id: 2, app: 'E-Commerce', message: 'Failed to load checkout session', version: '1.8.0', time: '15 mins ago', severity: 'high' },
{ id: 3, app: 'Fitness App', message: 'SocketException: Connection timed out', version: '0.9.5', time: '1 hour ago', severity: 'medium' },
]
function DashboardPage() {
const { data } = useSession()
const logout = useLogout()
const user = data?.user
const { data: sessionData } = useSession()
const user = sessionData?.user
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: ['dashboard', 'stats'],
queryFn: () => fetch('/api/dashboard/stats').then((r) => r.json()),
})
const { data: apps, isLoading: appsLoading } = useQuery({
queryKey: ['apps'],
queryFn: () => fetch('/api/apps').then((r) => r.json()),
})
return (
<Container size="md" py="xl">
<Stack gap="xl">
<Group justify="space-between">
<Title order={2}>Dashboard</Title>
<Button
variant="light"
color="red"
leftSection={<TbLogout size={16} />}
onClick={() => logout.mutate()}
loading={logout.isPending}
>
Logout
</Button>
</Group>
<Paper withBorder p="lg" radius="md">
<Group>
<Avatar color="blue" radius="xl" size="lg">
{user?.name?.charAt(0).toUpperCase()}
</Avatar>
<div>
<Group gap="xs">
<Text fw={500}>{user?.name}</Text>
<Badge color="red" variant="light" size="sm">SUPER ADMIN</Badge>
</Group>
<Text c="dimmed" size="sm">{user?.email}</Text>
</div>
<DashboardLayout>
<Container size="xl" py="lg">
<Stack gap="xl">
<Group justify="space-between" align="center">
<Stack gap={0}>
<Title order={2} className="gradient-text">Overview Dashboard</Title>
<Text size="sm" c="dimmed">Welcome back, {user?.name}. Here is what's happening today.</Text>
</Stack>
<Button
variant="gradient"
gradient={{ from: '#2563EB', to: '#7C3AED', deg: 135 }}
leftSection={<TbApps size={18} />}
radius="md"
component={Link}
to="/apps"
>
Manage All Apps
</Button>
</Group>
</Paper>
<SimpleGrid cols={{ base: 1, sm: 3 }}>
{stats.map((stat) => (
<Card key={stat.title} withBorder padding="lg" radius="md">
<Group justify="space-between" mb="xs">
<Text size="sm" c="dimmed" fw={500}>{stat.title}</Text>
<ThemeIcon variant="light" color={stat.color} size="sm">
<stat.icon size={14} />
</ThemeIcon>
</Group>
<Text fw={700} size="xl">{stat.value}</Text>
</Card>
))}
</SimpleGrid>
</Stack>
</Container>
{statsLoading ? (
<Loader size="xl" type="dots" mx="auto" mt="xl" />
) : (
<SimpleGrid cols={{ base: 1, sm: 3 }}>
<StatsCard
title="Total Applications"
value={stats?.totalApps || 0}
icon={TbApps}
color="brand-blue"
trend={{ value: stats?.trends?.totalApps.toString() || '0', positive: true }}
/>
<StatsCard
title="New Errors"
value={stats?.newErrors || 0}
icon={TbMessageReport}
color="brand-purple"
trend={{ value: stats?.trends?.newErrors.toString() || '0', positive: false }}
/>
<StatsCard
title="Active Users"
value={stats?.activeUsers || 0}
icon={TbUsers}
color="teal"
trend={{ value: stats?.trends?.activeUsers.toString() || '0', positive: true }}
/>
</SimpleGrid>
)}
<Group justify="space-between" mt="md">
<Title order={3}>Registered Applications</Title>
<Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />}>
View Report
</Button>
</Group>
{appsLoading ? (
<Loader size="xl" type="dots" mx="auto" />
) : (
<SimpleGrid cols={{ base: 1, md: 3 }}>
{apps?.map((app: any) => (
<AppCard key={app.id} {...app} />
))}
</SimpleGrid>
)}
<Group justify="space-between" mt="md">
<Title order={3}>Recent Error Reports</Title>
<Button variant="subtle" color="brand-blue" rightSection={<TbChevronRight size={16} />}>
View All Errors
</Button>
</Group>
<Paper withBorder radius="2xl" className="glass" p="md">
<Table className="data-table" verticalSpacing="md">
<Table.Thead>
<Table.Tr>
<Table.Th>Application</Table.Th>
<Table.Th>Error Message</Table.Th>
<Table.Th>Version</Table.Th>
<Table.Th>Time</Table.Th>
<Table.Th>Severity</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{recentErrors.map((error) => (
<Table.Tr key={error.id}>
<Table.Td>
<Text fw={600} size="sm">{error.app}</Text>
</Table.Td>
<Table.Td>
<Text size="sm" c="dimmed">{error.message}</Text>
</Table.Td>
<Table.Td>
<Badge variant="light" color="gray">v{error.version}</Badge>
</Table.Td>
<Table.Td>
<Text size="xs" c="dimmed">{error.time}</Text>
</Table.Td>
<Table.Td>
<Badge
color={error.severity === 'critical' ? 'red' : error.severity === 'high' ? 'orange' : 'yellow'}
variant="dot"
>
{error.severity.toUpperCase()}
</Badge>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Paper>
</Stack>
</Container>
</DashboardLayout>
)
}