feat: improve profile UI/UX and migrate to Mantine modals

This commit is contained in:
bipproduction
2026-02-07 15:05:28 +08:00
parent b9abcaadde
commit 718e7603d1
14 changed files with 752 additions and 760 deletions

View File

@@ -94,7 +94,9 @@ function DashboardApikeyComponent() {
setCreating(true);
const response = await apiClient.api.apikey.post({
name: newKeyName,
expiresAt: newKeyExpiresAt ? dayjs(newKeyExpiresAt).toISOString() : undefined,
expiresAt: newKeyExpiresAt
? dayjs(newKeyExpiresAt).toISOString()
: undefined,
});
if (response.data) {

View File

@@ -7,13 +7,13 @@ import {
Container,
Grid,
Group,
Modal,
Progress,
SimpleGrid,
Stack,
Text,
Title,
} from "@mantine/core";
import { modals } from "@mantine/modals";
import {
IconClock,
IconDatabase,
@@ -21,7 +21,6 @@ import {
IconUserCheck,
} from "@tabler/icons-react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useSnapshot } from "valtio";
import { authClient } from "@/utils/auth-client";
import { authStore } from "../../store/auth";
@@ -33,7 +32,19 @@ export const Route = createFileRoute("/dashboard/")({
function DashboardComponent() {
const snap = useSnapshot(authStore);
const navigate = useNavigate();
const [logoutModalOpen, setLogoutModalOpen] = useState(false);
const openLogoutModal = () =>
modals.openConfirmModal({
title: "Confirm Logout",
centered: true,
children: <Text size="sm">Are you sure you want to log out?</Text>,
labels: { confirm: "Logout", cancel: "Cancel" },
confirmProps: { color: "red" },
onConfirm: async () => {
await authClient.signOut();
navigate({ to: "/signin" });
},
});
// Mock data for dashboard stats
const statsData = [
@@ -84,11 +95,7 @@ function DashboardComponent() {
</Badge>
</div>
</Group>
<Button
variant="outline"
color="red"
onClick={() => setLogoutModalOpen(true)}
>
<Button variant="outline" color="red" onClick={openLogoutModal}>
Sign Out
</Button>
</Group>
@@ -197,28 +204,6 @@ function DashboardComponent() {
</Card>
</Grid.Col>
</Grid>
<Modal
opened={logoutModalOpen}
onClose={() => setLogoutModalOpen(false)}
title="Confirm Logout"
centered
>
<Text mb="md">Are you sure you want to log out?</Text>
<Group justify="flex-end">
<Button variant="outline" onClick={() => setLogoutModalOpen(false)}>
Cancel
</Button>
<Button
color="red"
onClick={() => {
authClient.signOut();
setLogoutModalOpen(false);
}}
>
Logout
</Button>
</Group>
</Modal>
</Container>
);
}

View File

@@ -1,20 +1,28 @@
import {
ActionIcon,
AppShell,
Avatar,
Box,
Burger,
Group,
Menu,
NavLink,
rem,
ScrollArea,
Stack,
Text,
Title
Tooltip,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { modals } from "@mantine/modals";
import {
IconChevronRight,
IconHome,
IconKey,
IconLogout,
IconSettings,
IconUsers
IconUser,
IconUsers,
} from "@tabler/icons-react";
import {
createFileRoute,
@@ -22,6 +30,9 @@ import {
useLocation,
useNavigate,
} from "@tanstack/react-router";
import { useSnapshot } from "valtio";
import { authStore } from "../../store/auth";
import { authClient } from "@/utils/auth-client";
export const Route = createFileRoute("/dashboard")({
component: DashboardLayout,
@@ -29,93 +40,279 @@ export const Route = createFileRoute("/dashboard")({
function DashboardLayout() {
const location = useLocation();
const navigate = useNavigate();
const snap = useSnapshot(authStore);
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const navigate = useNavigate();
const navItems = [
{ icon: IconHome, label: "Beranda", to: "/dashboard" },
{ icon: IconUsers, label: "Pengguna", to: "/dashboard/users" },
{ icon: IconKey, label: "API Key", to: "/dashboard/apikey" },
{ icon: IconSettings, label: "Pengaturan", to: "/dashboard/settings" },
{
icon: IconHome,
label: "Beranda",
to: "/dashboard",
description: "Ringkasan sistem & statistik",
},
{
icon: IconUsers,
label: "Pengguna",
to: "/dashboard/users",
description: "Kelola akun & hak akses",
},
{
icon: IconKey,
label: "API Key",
to: "/dashboard/apikey",
description: "Manajemen kunci akses API",
},
{
icon: IconSettings,
label: "Pengaturan",
to: "/dashboard/settings",
description: "Konfigurasi sistem",
},
];
const handleLogout = async () => {
modals.openConfirmModal({
title: "Konfirmasi Keluar",
centered: true,
children: (
<Text size="sm">
Apakah Anda yakin ingin keluar dari sistem? Sesi Anda akan berakhir.
</Text>
),
labels: { confirm: "Keluar", cancel: "Batal" },
confirmProps: { color: "red" },
onConfirm: async () => {
await authClient.signOut();
navigate({ to: "/signin" });
},
});
};
const isActive = (path: string) => {
const current = location.pathname;
if (path === "/dashboard") {
return current === "/dashboard";
}
return current === path || current.startsWith(`${path}/`);
if (path === "/dashboard")
return current === "/dashboard" || current === "/dashboard/";
return current.startsWith(path);
};
return (
<AppShell
header={{ height: 70 }}
navbar={{
width: 300,
width: 280,
breakpoint: "sm",
collapsed: { mobile: !mobileOpened, desktop: !desktopOpened },
}}
padding="md"
header={{ height: 60 }}
transitionDuration={500}
transitionTimingFunction="ease"
>
<AppShell.Header>
<AppShell.Header
bg="rgba(26, 26, 26, 0.8)"
style={{
backdropFilter: "blur(10px)",
borderBottom: "1px solid rgba(251, 240, 223, 0.1)",
}}
>
<Group h="100%" px="md" justify="space-between">
<Group>
<Group gap="xs">
<Burger
opened={mobileOpened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
color="#f3d5a3"
/>
<Burger
opened={desktopOpened}
onClick={toggleDesktop}
visibleFrom="sm"
size="sm"
color="#f3d5a3"
/>
<Title order={3}>Dashboard</Title>
<Box visibleFrom="xs" ml="xs">
<Text
fw={800}
size="xl"
c="#f3d5a3"
style={{ letterSpacing: "-0.5px" }}
>
ADMIN
<Text span c="#fbf0df">
PANEL
</Text>
</Text>
</Box>
</Group>
<Group>
<ActionIcon variant="subtle" size="lg">
<IconSettings
style={{ width: rem(20), height: rem(20) }}
stroke={1.5}
/>
</ActionIcon>
<Group gap="md">
<Menu
shadow="md"
width={200}
position="bottom-end"
transitionProps={{ transition: "pop-top-right" }}
>
<Menu.Target>
<Group
gap="xs"
style={{ cursor: "pointer" }}
p="xs"
hover-bg="rgba(255,255,255,0.05)"
>
<div
style={{ textAlign: "right" }}
className="visible-from-sm"
>
<Text size="sm" fw={600} c="#fbf0df">
{snap.user?.name}
</Text>
<Text size="xs" c="dimmed">
Administrator
</Text>
</div>
<Avatar
src={snap.user?.image}
radius="xl"
size="md"
style={{ border: "2px solid #f3d5a3" }}
>
{snap.user?.name?.charAt(0)}
</Avatar>
</Group>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Akun</Menu.Label>
<Menu.Item
leftSection={
<IconUser style={{ width: rem(14), height: rem(14) }} />
}
onClick={() => navigate({ to: "/profile" })}
>
Profil Saya
</Menu.Item>
<Menu.Item
leftSection={
<IconSettings style={{ width: rem(14), height: rem(14) }} />
}
onClick={() => navigate({ to: "/dashboard/settings" })}
>
Pengaturan
</Menu.Item>
<Menu.Divider />
<Menu.Label>Bahaya</Menu.Label>
<Menu.Item
color="red"
leftSection={
<IconLogout style={{ width: rem(14), height: rem(14) }} />
}
onClick={handleLogout}
>
Keluar Sistem
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>
</Group>
</AppShell.Header>
<AppShell.Navbar p="md">
<ScrollArea h="calc(100vh - 120px)">
<Group mb="lg">
<Text fw={500} size="lg">
Navigasi Utama
</Text>
</Group>
{navItems.map((item) => (
<NavLink
key={item.to}
onClick={() => {
navigate({ to: item.to });
}}
leftSection={
<item.icon
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
<AppShell.Navbar
p="md"
bg="rgba(20, 20, 20, 1)"
style={{ borderRight: "1px solid rgba(251, 240, 223, 0.1)" }}
>
<AppShell.Section grow component={ScrollArea} mx="-md" px="md">
<Stack gap="xs" mt="md">
{navItems.map((item) => (
<Tooltip
key={item.to}
label={item.description}
position="right"
disabled={!desktopOpened}
openDelay={500}
>
<NavLink
onClick={() => {
navigate({ to: item.to });
if (mobileOpened) toggleMobile();
}}
leftSection={
<item.icon
style={{ width: rem(20), height: rem(20) }}
stroke={1.5}
/>
}
label={
<Box>
<Text size="sm" fw={isActive(item.to) ? 700 : 500}>
{item.label}
</Text>
</Box>
}
rightSection={<IconChevronRight size="0.8rem" stroke={1.5} />}
active={isActive(item.to)}
variant="filled"
color="orange"
styles={{
root: {
borderRadius: rem(8),
marginBottom: rem(4),
backgroundColor: isActive(item.to)
? "rgba(243, 213, 163, 0.1)"
: "transparent",
color: isActive(item.to) ? "#f3d5a3" : "#fbf0df",
"&:hover": {
backgroundColor: "rgba(243, 213, 163, 0.05)",
},
},
label: {
fontSize: rem(14),
},
}}
/>
}
label={item.label}
active={isActive(item.to)}
/>
))}
</ScrollArea>
</Tooltip>
))}
</Stack>
</AppShell.Section>
<AppShell.Section
style={{ borderTop: "1px solid rgba(251, 240, 223, 0.1)" }}
pt="md"
>
<NavLink
label="Pusat Bantuan"
leftSection={
<IconSettings
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
/>
}
styles={{ root: { borderRadius: rem(8) } }}
/>
<NavLink
label="Keluar"
onClick={handleLogout}
leftSection={
<IconLogout
style={{ width: rem(18), height: rem(18) }}
stroke={1.5}
color="red"
/>
}
c="red"
styles={{ root: { borderRadius: rem(8) } }}
/>
</AppShell.Section>
</AppShell.Navbar>
<AppShell.Main>
<Outlet />
<AppShell.Main bg="rgba(15, 15, 15, 1)">
<Box p="lg" style={{ minHeight: "calc(100vh - 100px)" }}>
<Outlet />
</Box>
</AppShell.Main>
</AppShell>
);