Refactor: move AppShell to global layout, add breadcrumbs, and restructure profile routes

This commit is contained in:
2026-03-26 17:10:40 +08:00
parent 0d0dc187a5
commit 34804127c5
20 changed files with 548 additions and 992 deletions

View File

@@ -1,8 +1,10 @@
import { import {
ActionIcon, ActionIcon,
Anchor,
Avatar, Avatar,
Badge, Badge,
Box, Box,
Breadcrumbs,
Divider, Divider,
Group, Group,
Text, Text,
@@ -20,14 +22,70 @@ interface HeaderProps {
} }
export function Header({ onSidebarToggle }: HeaderProps) { export function Header({ onSidebarToggle }: HeaderProps) {
const _location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark"; const dark = colorScheme === "dark";
const pathnames = location.pathname.split("/").filter((x) => x);
const breadcrumbItems = [
<Anchor
key="home"
onClick={() => navigate({ to: "/" })}
c="white"
size="sm"
underline="hover"
>
Desa Darmasaba
</Anchor>,
...pathnames.map((value, index) => {
const to = `/${pathnames.slice(0, index + 1).join("/")}`;
const isLast = index === pathnames.length - 1;
// Map route path to human-readable label
const labelMap: Record<string, string> = {
"kinerja-divisi": "Kinerja Divisi",
"pengaduan-layanan-publik": "Pengaduan & Layanan Publik",
"jenna-analytic": "Jenna Analytic",
"demografi-pekerjaan": "Demografi & Kependudukan",
"keuangan-anggaran": "Keuangan & Anggaran",
bumdes: "Bumdes & UMKM",
sosial: "Sosial",
keamanan: "Keamanan",
bantuan: "Bantuan",
pengaturan: "Pengaturan",
umum: "Umum",
notifikasi: "Notifikasi",
"akses-dan-tim": "Akses & Tim",
profile: "Profil",
edit: "Edit",
};
const label =
labelMap[value] || value.charAt(0).toUpperCase() + value.slice(1);
return isLast ? (
<Text key={to} c="white" size="sm" fw={600}>
{label}
</Text>
) : (
<Anchor
key={to}
onClick={() => navigate({ to })}
c="white"
size="sm"
underline="hover"
>
{label}
</Anchor>
);
}),
];
return ( return (
<Group justify="space-between" w="100%"> <Group justify="space-between" w="100%">
{/* Title */} {/* Title & Breadcrumbs */}
<Group gap="md"> <Group gap="md">
<ActionIcon <ActionIcon
onClick={onSidebarToggle} onClick={onSidebarToggle}
@@ -42,6 +100,18 @@ export function Header({ onSidebarToggle }: HeaderProps) {
style={{ width: "70%", height: "70%" }} style={{ width: "70%", height: "70%" }}
/> />
</ActionIcon> </ActionIcon>
<Breadcrumbs
separator={
<Text c="white" size="xs">
/
</Text>
}
styles={{
separator: { color: "white" },
}}
>
{breadcrumbItems}
</Breadcrumbs>
</Group> </Group>
{/* Right Section */} {/* Right Section */}

View File

@@ -0,0 +1,66 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import type React from "react";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
interface MainLayoutProps {
children: React.ReactNode;
}
export function MainLayout({ children }: MainLayoutProps) {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
{children}
</AppShell.Main>
</AppShell>
);
}

View File

@@ -20,6 +20,7 @@ import { Route as JennaAnalyticRouteImport } from './routes/jenna-analytic'
import { Route as DemografiPekerjaanRouteImport } from './routes/demografi-pekerjaan' import { Route as DemografiPekerjaanRouteImport } from './routes/demografi-pekerjaan'
import { Route as BumdesRouteImport } from './routes/bumdes' import { Route as BumdesRouteImport } from './routes/bumdes'
import { Route as BantuanRouteImport } from './routes/bantuan' import { Route as BantuanRouteImport } from './routes/bantuan'
import { Route as ProfileRouteRouteImport } from './routes/profile/route'
import { Route as PengaturanRouteRouteImport } from './routes/pengaturan/route' import { Route as PengaturanRouteRouteImport } from './routes/pengaturan/route'
import { Route as AdminRouteRouteImport } from './routes/admin/route' import { Route as AdminRouteRouteImport } from './routes/admin/route'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
@@ -91,6 +92,11 @@ const BantuanRoute = BantuanRouteImport.update({
path: '/bantuan', path: '/bantuan',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ProfileRouteRoute = ProfileRouteRouteImport.update({
id: '/profile',
path: '/profile',
getParentRoute: () => rootRouteImport,
} as any)
const PengaturanRouteRoute = PengaturanRouteRouteImport.update({ const PengaturanRouteRoute = PengaturanRouteRouteImport.update({
id: '/pengaturan', id: '/pengaturan',
path: '/pengaturan', path: '/pengaturan',
@@ -112,9 +118,9 @@ const UsersIndexRoute = UsersIndexRouteImport.update({
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ProfileIndexRoute = ProfileIndexRouteImport.update({ const ProfileIndexRoute = ProfileIndexRouteImport.update({
id: '/profile/', id: '/',
path: '/profile/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => ProfileRouteRoute,
} as any) } as any)
const AdminIndexRoute = AdminIndexRouteImport.update({ const AdminIndexRoute = AdminIndexRouteImport.update({
id: '/', id: '/',
@@ -127,9 +133,9 @@ const UsersIdRoute = UsersIdRouteImport.update({
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const ProfileEditRoute = ProfileEditRouteImport.update({ const ProfileEditRoute = ProfileEditRouteImport.update({
id: '/profile/edit', id: '/edit',
path: '/profile/edit', path: '/edit',
getParentRoute: () => rootRouteImport, getParentRoute: () => ProfileRouteRoute,
} as any) } as any)
const PengaturanUmumRoute = PengaturanUmumRouteImport.update({ const PengaturanUmumRoute = PengaturanUmumRouteImport.update({
id: '/umum', id: '/umum',
@@ -171,6 +177,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute '/': typeof IndexRoute
'/admin': typeof AdminRouteRouteWithChildren '/admin': typeof AdminRouteRouteWithChildren
'/pengaturan': typeof PengaturanRouteRouteWithChildren '/pengaturan': typeof PengaturanRouteRouteWithChildren
'/profile': typeof ProfileRouteRouteWithChildren
'/bantuan': typeof BantuanRoute '/bantuan': typeof BantuanRoute
'/bumdes': typeof BumdesRoute '/bumdes': typeof BumdesRoute
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute '/demografi-pekerjaan': typeof DemografiPekerjaanRoute
@@ -227,6 +234,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute '/': typeof IndexRoute
'/admin': typeof AdminRouteRouteWithChildren '/admin': typeof AdminRouteRouteWithChildren
'/pengaturan': typeof PengaturanRouteRouteWithChildren '/pengaturan': typeof PengaturanRouteRouteWithChildren
'/profile': typeof ProfileRouteRouteWithChildren
'/bantuan': typeof BantuanRoute '/bantuan': typeof BantuanRoute
'/bumdes': typeof BumdesRoute '/bumdes': typeof BumdesRoute
'/demografi-pekerjaan': typeof DemografiPekerjaanRoute '/demografi-pekerjaan': typeof DemografiPekerjaanRoute
@@ -257,6 +265,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/admin' | '/admin'
| '/pengaturan' | '/pengaturan'
| '/profile'
| '/bantuan' | '/bantuan'
| '/bumdes' | '/bumdes'
| '/demografi-pekerjaan' | '/demografi-pekerjaan'
@@ -312,6 +321,7 @@ export interface FileRouteTypes {
| '/' | '/'
| '/admin' | '/admin'
| '/pengaturan' | '/pengaturan'
| '/profile'
| '/bantuan' | '/bantuan'
| '/bumdes' | '/bumdes'
| '/demografi-pekerjaan' | '/demografi-pekerjaan'
@@ -341,6 +351,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute IndexRoute: typeof IndexRoute
AdminRouteRoute: typeof AdminRouteRouteWithChildren AdminRouteRoute: typeof AdminRouteRouteWithChildren
PengaturanRouteRoute: typeof PengaturanRouteRouteWithChildren PengaturanRouteRoute: typeof PengaturanRouteRouteWithChildren
ProfileRouteRoute: typeof ProfileRouteRouteWithChildren
BantuanRoute: typeof BantuanRoute BantuanRoute: typeof BantuanRoute
BumdesRoute: typeof BumdesRoute BumdesRoute: typeof BumdesRoute
DemografiPekerjaanRoute: typeof DemografiPekerjaanRoute DemografiPekerjaanRoute: typeof DemografiPekerjaanRoute
@@ -352,9 +363,7 @@ export interface RootRouteChildren {
SigninRoute: typeof SigninRoute SigninRoute: typeof SigninRoute
SignupRoute: typeof SignupRoute SignupRoute: typeof SignupRoute
SosialRoute: typeof SosialRoute SosialRoute: typeof SosialRoute
ProfileEditRoute: typeof ProfileEditRoute
UsersIdRoute: typeof UsersIdRoute UsersIdRoute: typeof UsersIdRoute
ProfileIndexRoute: typeof ProfileIndexRoute
UsersIndexRoute: typeof UsersIndexRoute UsersIndexRoute: typeof UsersIndexRoute
} }
@@ -437,6 +446,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof BantuanRouteImport preLoaderRoute: typeof BantuanRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/profile': {
id: '/profile'
path: '/profile'
fullPath: '/profile'
preLoaderRoute: typeof ProfileRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/pengaturan': { '/pengaturan': {
id: '/pengaturan' id: '/pengaturan'
path: '/pengaturan' path: '/pengaturan'
@@ -467,10 +483,10 @@ declare module '@tanstack/react-router' {
} }
'/profile/': { '/profile/': {
id: '/profile/' id: '/profile/'
path: '/profile' path: '/'
fullPath: '/profile/' fullPath: '/profile/'
preLoaderRoute: typeof ProfileIndexRouteImport preLoaderRoute: typeof ProfileIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof ProfileRouteRoute
} }
'/admin/': { '/admin/': {
id: '/admin/' id: '/admin/'
@@ -488,10 +504,10 @@ declare module '@tanstack/react-router' {
} }
'/profile/edit': { '/profile/edit': {
id: '/profile/edit' id: '/profile/edit'
path: '/profile/edit' path: '/edit'
fullPath: '/profile/edit' fullPath: '/profile/edit'
preLoaderRoute: typeof ProfileEditRouteImport preLoaderRoute: typeof ProfileEditRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof ProfileRouteRoute
} }
'/pengaturan/umum': { '/pengaturan/umum': {
id: '/pengaturan/umum' id: '/pengaturan/umum'
@@ -581,10 +597,25 @@ const PengaturanRouteRouteWithChildren = PengaturanRouteRoute._addFileChildren(
PengaturanRouteRouteChildren, PengaturanRouteRouteChildren,
) )
interface ProfileRouteRouteChildren {
ProfileEditRoute: typeof ProfileEditRoute
ProfileIndexRoute: typeof ProfileIndexRoute
}
const ProfileRouteRouteChildren: ProfileRouteRouteChildren = {
ProfileEditRoute: ProfileEditRoute,
ProfileIndexRoute: ProfileIndexRoute,
}
const ProfileRouteRouteWithChildren = ProfileRouteRoute._addFileChildren(
ProfileRouteRouteChildren,
)
const rootRouteChildren: RootRouteChildren = { const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute, IndexRoute: IndexRoute,
AdminRouteRoute: AdminRouteRouteWithChildren, AdminRouteRoute: AdminRouteRouteWithChildren,
PengaturanRouteRoute: PengaturanRouteRouteWithChildren, PengaturanRouteRoute: PengaturanRouteRouteWithChildren,
ProfileRouteRoute: ProfileRouteRouteWithChildren,
BantuanRoute: BantuanRoute, BantuanRoute: BantuanRoute,
BumdesRoute: BumdesRoute, BumdesRoute: BumdesRoute,
DemografiPekerjaanRoute: DemografiPekerjaanRoute, DemografiPekerjaanRoute: DemografiPekerjaanRoute,
@@ -596,9 +627,7 @@ const rootRouteChildren: RootRouteChildren = {
SigninRoute: SigninRoute, SigninRoute: SigninRoute,
SignupRoute: SignupRoute, SignupRoute: SignupRoute,
SosialRoute: SosialRoute, SosialRoute: SosialRoute,
ProfileEditRoute: ProfileEditRoute,
UsersIdRoute: UsersIdRoute, UsersIdRoute: UsersIdRoute,
ProfileIndexRoute: ProfileIndexRoute,
UsersIndexRoute: UsersIndexRoute, UsersIndexRoute: UsersIndexRoute,
} }
export const routeTree = rootRouteImport export const routeTree = rootRouteImport

View File

@@ -3,7 +3,12 @@ import { protectedRouteMiddleware } from "@/middleware/authMiddleware";
import { authStore } from "@/store/auth"; import { authStore } from "@/store/auth";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import "@mantine/dates/styles.css"; import "@mantine/dates/styles.css";
import { createRootRoute, Outlet } from "@tanstack/react-router"; import {
createRootRoute,
Outlet,
useRouterState,
} from "@tanstack/react-router";
import { MainLayout } from "@/components/layout/main-layout";
export const Route = createRootRoute({ export const Route = createRootRoute({
component: RootComponent, component: RootComponent,
@@ -21,5 +26,18 @@ export const Route = createRootRoute({
}); });
function RootComponent() { function RootComponent() {
return <Outlet />; const routerState = useRouterState();
const isPublicRoute = ["/signin", "/signup", "/admin", "/profile"].some(
(path) => routerState.location.pathname.startsWith(path),
);
if (isPublicRoute) {
return <Outlet />;
}
return (
<MainLayout>
<Outlet />
</MainLayout>
);
} }

View File

@@ -4,7 +4,6 @@ import {
Box, Box,
Button, Button,
Card, Card,
Container,
Grid, Grid,
Group, Group,
Progress, Progress,
@@ -55,7 +54,7 @@ function DashboardComponent() {
]; ];
return ( return (
<Container size="lg" py="xl"> <Box py="xl">
<Title <Title
order={1} order={1}
ta="center" ta="center"
@@ -195,6 +194,6 @@ function DashboardComponent() {
</Card> </Card>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Container> </Box>
); );
} }

View File

@@ -315,9 +315,7 @@ function DashboardLayout() {
</AppShell.Navbar> </AppShell.Navbar>
<AppShell.Main> <AppShell.Main>
<Box p="lg" style={{ minHeight: "calc(100vh - 100px)" }}> <Outlet />
<Outlet />
</Box>
</AppShell.Main> </AppShell.Main>
</AppShell> </AppShell>
); );

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import HelpPage from "@/components/help-page"; import HelpPage from "@/components/help-page";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/bantuan")({ export const Route = createFileRoute("/bantuan")({
component: BantuanRoute, component: HelpPage,
}); });
function BantuanRoute() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<HelpPage />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import BumdesPage from "@/components/bumdes-page"; import BumdesPage from "@/components/bumdes-page";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/bumdes")({ export const Route = createFileRoute("/bumdes")({
component: BumdesRoute, component: BumdesPage,
}); });
function BumdesRoute() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<BumdesPage />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
import DemografiPekerjaan from "../components/demografi-pekerjaan"; import DemografiPekerjaan from "../components/demografi-pekerjaan";
export const Route = createFileRoute("/demografi-pekerjaan")({ export const Route = createFileRoute("/demografi-pekerjaan")({
component: DemografiPekerjaanPage, component: DemografiPekerjaan,
}); });
function DemografiPekerjaanPage() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<DemografiPekerjaan />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,72 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
import { DashboardContent } from "@/components/dashboard-content"; import { DashboardContent } from "@/components/dashboard-content";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
component: DashboardPage, component: DashboardContent,
}); });
function DashboardPage() {
const [opened, { toggle }] = useDisclosure();
const [sidebarCollapsed, setSidebarCollapsed] = useDisclosure(false);
const [clickCount, setClickCount] = useState(0);
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
const handleMainClick = () => {
if (!sidebarCollapsed) {
const newCount = clickCount + 1;
setClickCount(newCount);
if (newCount === 2) {
setSidebarCollapsed.toggle();
setClickCount(0);
} else {
setTimeout(() => setClickCount(0), 300);
}
}
};
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} hiddenFrom="sm" size="sm" />
<Header onSidebarToggle={setSidebarCollapsed.toggle} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<DashboardContent />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import JennaAnalytic from "@/components/jenna-analytic"; import JennaAnalytic from "@/components/jenna-analytic";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/jenna-analytic")({ export const Route = createFileRoute("/jenna-analytic")({
component: JennaAnalyticPage, component: JennaAnalytic,
}); });
function JennaAnalyticPage() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<JennaAnalytic />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import KeamananPage from "@/components/keamanan-page"; import KeamananPage from "@/components/keamanan-page";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/keamanan")({ export const Route = createFileRoute("/keamanan")({
component: KeamananRoute, component: KeamananPage,
}); });
function KeamananRoute() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<KeamananPage />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import KeuanganAnggaran from "@/components/keuangan-anggaran"; import KeuanganAnggaran from "@/components/keuangan-anggaran";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/keuangan-anggaran")({ export const Route = createFileRoute("/keuangan-anggaran")({
component: KeuanganAnggaranPage, component: KeuanganAnggaran,
}); });
function KeuanganAnggaranPage() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<KeuanganAnggaran />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import KinerjaDivisi from "@/components/kinerja-divisi"; import KinerjaDivisi from "@/components/kinerja-divisi";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/kinerja-divisi")({ export const Route = createFileRoute("/kinerja-divisi")({
component: KinerjaDivisiPage, component: KinerjaDivisi,
}); });
function KinerjaDivisiPage() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<KinerjaDivisi />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik"; import PengaduanLayananPublik from "@/components/pengaduan-layanan-publik";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/pengaduan-layanan-publik")({ export const Route = createFileRoute("/pengaduan-layanan-publik")({
component: PengaduanLayananPublikPage, component: PengaduanLayananPublik,
}); });
function PengaduanLayananPublikPage() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<PengaduanLayananPublik />
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,84 +1,5 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core"; import { createFileRoute, Outlet } from "@tanstack/react-router";
import { useMediaQuery } from "@mantine/hooks";
import {
createFileRoute,
Outlet,
useRouterState,
} from "@tanstack/react-router";
import { useEffect } from "react";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/pengaturan")({ export const Route = createFileRoute("/pengaturan")({
component: PengaturanLayout, component: Outlet,
}); });
function PengaturanLayout() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const isMobile = useMediaQuery("(max-width: 48em)");
const _routerState = useRouterState();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
// Auto close navbar on route change (mobile only)
useEffect(() => {
if (isMobile && opened) {
toggleMobile();
}
}, [isMobile, opened, toggleMobile]);
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="lg" align="center" wrap="nowrap">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<div className="p-2">
<Outlet />
</div>
</AppShell.Main>
</AppShell>
);
}

View File

@@ -1,7 +1,7 @@
import { import {
Box,
Button, Button,
Card, Card,
Container,
Divider, Divider,
Group, Group,
Stack, Stack,
@@ -63,77 +63,72 @@ function EditProfile() {
}; };
return ( return (
<Container size="sm" py={50}> <Stack gap="xl" px={"lg"}>
<Stack gap="xl"> <Group justify="space-between" align="center">
<Group justify="space-between" align="center"> <Box>
<Box> <Title order={1} c="orange.6">
<Title order={1} c="orange.6"> Edit Profil
Edit Profil </Title>
</Title> <Text c="dimmed" size="sm">
<Text c="dimmed" size="sm"> Perbarui informasi profil publik Anda
Perbarui informasi profil publik Anda </Text>
</Text> </Box>
</Box> <Button
<Button variant="subtle"
variant="subtle" color="gray"
color="gray" leftSection={<IconChevronLeft size={18} />}
leftSection={<IconChevronLeft size={18} />} onClick={() => navigate({ to: "/profile" })}
onClick={() => navigate({ to: "/profile" })}
>
Kembali
</Button>
</Group>
<Divider style={{ opacity: 0.1 }} />
<Card
withBorder
radius="md"
p="xl"
style={{ border: "1px solid var(--mantine-color-default-border)" }}
> >
<form onSubmit={form.onSubmit(handleUpdateProfile)}> Kembali
<Stack gap="md"> </Button>
<TextInput </Group>
label="Nama Lengkap"
placeholder="Masukkan nama lengkap Anda" <Divider style={{ opacity: 0.1 }} />
{...form.getInputProps("name")}
styles={{ <Card
label: { marginBottom: 8 }, withBorder
input: { radius="md"
backgroundColor: "var(--mantine-color-default-soft)", p="xl"
}, style={{ border: "1px solid var(--mantine-color-default-border)" }}
}} >
/> <form onSubmit={form.onSubmit(handleUpdateProfile)}>
<TextInput <Stack gap="md">
label="URL Foto Profil" <TextInput
placeholder="https://example.com/photo.jpg" label="Nama Lengkap"
{...form.getInputProps("image")} placeholder="Masukkan nama lengkap Anda"
styles={{ {...form.getInputProps("name")}
label: { marginBottom: 8 }, styles={{
input: { label: { marginBottom: 8 },
backgroundColor: "var(--mantine-color-default-soft)", input: {
}, backgroundColor: "var(--mantine-color-default-soft)",
}} },
/> }}
<Button />
type="submit" <TextInput
fullWidth label="URL Foto Profil"
mt="lg" placeholder="https://example.com/photo.jpg"
size="md" {...form.getInputProps("image")}
color="orange" styles={{
loading={isUpdating} label: { marginBottom: 8 },
leftSection={<IconEdit size={18} />} input: {
> backgroundColor: "var(--mantine-color-default-soft)",
Simpan Perubahan },
</Button> }}
</Stack> />
</form> <Button
</Card> type="submit"
</Stack> fullWidth
</Container> mt="lg"
size="md"
color="orange"
loading={isUpdating}
leftSection={<IconEdit size={18} />}
>
Simpan Perubahan
</Button>
</Stack>
</form>
</Card>
</Stack>
); );
} }
// Need Box from @mantine/core
import { Box } from "@mantine/core";

View File

@@ -6,7 +6,6 @@ import {
Button, Button,
Card, Card,
Code, Code,
Container,
Divider, Divider,
Grid, Grid,
Group, Group,
@@ -141,213 +140,211 @@ function Profile() {
); );
return ( return (
<Container size="md" py={50}> <Stack gap="xl" px={"lg"}>
<Stack gap="xl"> {/* Header Section */}
{/* Header Section */} <Group justify="space-between" align="center">
<Group justify="space-between" align="center"> <Box>
<Box> <Title order={1} c="orange.6">
<Title order={1} c="orange.6"> Profil Saya
Profil Saya </Title>
</Title> <Text c="dimmed" size="sm">
<Text c="dimmed" size="sm"> Kelola informasi akun dan pengaturan keamanan Anda
Kelola informasi akun dan pengaturan keamanan Anda </Text>
</Text> </Box>
</Box> <Group>
<Group> {snap.user?.role === "admin" && (
{snap.user?.role === "admin" && (
<Button
variant="light"
color="orange"
leftSection={<IconDashboard size={18} />}
onClick={() => navigate({ to: "/admin" })}
>
Admin Panel
</Button>
)}
<Button <Button
variant="light" variant="light"
color="blue" color="orange"
leftSection={<IconEdit size={18} />} leftSection={<IconDashboard size={18} />}
onClick={() => navigate({ to: "/profile/edit" })} onClick={() => navigate({ to: "/admin" })}
> >
Edit Profil Admin Panel
</Button> </Button>
<Button )}
variant="outline" <Button
color="red" variant="light"
leftSection={<IconLogout size={18} />} color="blue"
onClick={openLogoutModal} leftSection={<IconEdit size={18} />}
> onClick={() => navigate({ to: "/profile/edit" })}
Keluar >
</Button> Edit Profil
</Group> </Button>
<Button
variant="outline"
color="red"
leftSection={<IconLogout size={18} />}
onClick={openLogoutModal}
>
Keluar
</Button>
</Group> </Group>
</Group>
<Divider style={{ opacity: 0.1 }} /> <Divider style={{ opacity: 0.1 }} />
{/* Profile Overview Card */} {/* Profile Overview Card */}
<Card withBorder radius="lg" p={0} style={{ overflow: "hidden" }}> <Card withBorder radius="lg" p={0} style={{ overflow: "hidden" }}>
<Box <Box
h={120} h={120}
style={{ style={{
background: background:
"linear-gradient(45deg, var(--mantine-color-gray-filled) 0%, var(--mantine-color-dark-filled) 100%)", "linear-gradient(45deg, var(--mantine-color-gray-filled) 0%, var(--mantine-color-dark-filled) 100%)",
borderBottom: "1px solid var(--mantine-color-default-border)", borderBottom: "1px solid var(--mantine-color-default-border)",
}} }}
/> />
<Box px="xl" pb="xl" style={{ marginTop: rem(-60) }}> <Box px="xl" pb="xl" style={{ marginTop: rem(-60) }}>
<Group align="flex-end" gap="xl" mb="md"> <Group align="flex-end" gap="xl" mb="md">
<Avatar <Avatar
src={snap.user?.image} src={snap.user?.image}
size={120} size={120}
radius={120} radius={120}
style={{ style={{
border: "4px solid var(--mantine-color-body)", border: "4px solid var(--mantine-color-body)",
boxShadow: "var(--mantine-shadow-md)", boxShadow: "var(--mantine-shadow-md)",
}} }}
> >
{snap.user?.name?.charAt(0).toUpperCase()} {snap.user?.name?.charAt(0).toUpperCase()}
</Avatar> </Avatar>
<Stack gap={0} pb="md"> <Stack gap={0} pb="md">
<Title order={2}>{snap.user?.name}</Title> <Title order={2}>{snap.user?.name}</Title>
<Group gap="xs"> <Group gap="xs">
<Text c="dimmed" size="sm"> <Text c="dimmed" size="sm">
{snap.user?.email} {snap.user?.email}
</Text>
<Text c="dimmed" size="xs">
</Text>
<Badge
variant="dot"
color={snap.user?.role === "admin" ? "orange" : "blue"}
size="sm"
>
{snap.user?.role || "user"}
</Badge>
</Group>
</Stack>
</Group>
</Box>
</Card>
<Grid gutter="lg">
<Grid.Col span={{ base: 12, md: 7 }}>
<Stack gap="md">
<Title order={4} c="orange.6">
Informasi Identitas
</Title>
<Grid gutter="sm">
<Grid.Col span={6}>
<InfoField
icon={IconUser}
label="Nama Lengkap"
value={snap.user?.name}
/>
</Grid.Col>
<Grid.Col span={6}>
<InfoField
icon={IconShield}
label="Peran"
value={snap.user?.role || "User"}
/>
</Grid.Col>
<Grid.Col span={12}>
<InfoField
icon={IconAt}
label="Alamat Email"
value={snap.user?.email}
copyable
id="email"
/>
</Grid.Col>
<Grid.Col span={12}>
<InfoField
icon={IconId}
label="Unique User ID"
value={snap.user?.id}
copyable
id="userid"
/>
</Grid.Col>
</Grid>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 5 }}>
<Stack gap="md">
<Title order={4} c="orange.6">
Keamanan & Sesi
</Title>
<Card
withBorder
radius="md"
p="lg"
style={{
border: "1px solid var(--mantine-color-default-border)",
}}
>
<Stack gap="md">
<Box>
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>
Sesi Saat Ini
</Text> </Text>
<Text c="dimmed" size="xs"> <Group justify="space-between" align="center">
<Badge color="green" variant="light">
Aktif Sekarang
</Badge>
<Text size="xs" c="dimmed">
ID: {snap.session?.id?.substring(0, 8)}...
</Text>
</Group>
</Box>
<Box>
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>
Session Token
</Text> </Text>
<Badge <Group gap="xs" wrap="nowrap">
variant="dot" <Code
color={snap.user?.role === "admin" ? "orange" : "blue"} block
size="sm" style={{
> fontSize: rem(11),
{snap.user?.role || "user"} flex: 1,
</Badge> }}
</Group> >
{snap.session?.token
? `${snap.session.token.substring(0, 32)}...`
: "N/A"}
</Code>
<ActionIcon
variant="light"
color="gray"
onClick={() =>
snap.session?.token &&
copyToClipboard(snap.session.token, "token")
}
>
{copied === "token" ? (
<IconCheck size={16} />
) : (
<IconCopy size={16} />
)}
</ActionIcon>
</Group>
</Box>
<Button
variant="light"
color="gray"
fullWidth
leftSection={<IconExternalLink size={16} />}
>
Riwayat Sesi
</Button>
</Stack> </Stack>
</Group> </Card>
</Box> </Stack>
</Card> </Grid.Col>
</Grid>
<Grid gutter="lg"> </Stack>
<Grid.Col span={{ base: 12, md: 7 }}>
<Stack gap="md">
<Title order={4} c="orange.6">
Informasi Identitas
</Title>
<Grid gutter="sm">
<Grid.Col span={6}>
<InfoField
icon={IconUser}
label="Nama Lengkap"
value={snap.user?.name}
/>
</Grid.Col>
<Grid.Col span={6}>
<InfoField
icon={IconShield}
label="Peran"
value={snap.user?.role || "User"}
/>
</Grid.Col>
<Grid.Col span={12}>
<InfoField
icon={IconAt}
label="Alamat Email"
value={snap.user?.email}
copyable
id="email"
/>
</Grid.Col>
<Grid.Col span={12}>
<InfoField
icon={IconId}
label="Unique User ID"
value={snap.user?.id}
copyable
id="userid"
/>
</Grid.Col>
</Grid>
</Stack>
</Grid.Col>
<Grid.Col span={{ base: 12, md: 5 }}>
<Stack gap="md">
<Title order={4} c="orange.6">
Keamanan & Sesi
</Title>
<Card
withBorder
radius="md"
p="lg"
style={{
border: "1px solid var(--mantine-color-default-border)",
}}
>
<Stack gap="md">
<Box>
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>
Sesi Saat Ini
</Text>
<Group justify="space-between" align="center">
<Badge color="green" variant="light">
Aktif Sekarang
</Badge>
<Text size="xs" c="dimmed">
ID: {snap.session?.id?.substring(0, 8)}...
</Text>
</Group>
</Box>
<Box>
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={8}>
Session Token
</Text>
<Group gap="xs" wrap="nowrap">
<Code
block
style={{
fontSize: rem(11),
flex: 1,
}}
>
{snap.session?.token
? `${snap.session.token.substring(0, 32)}...`
: "N/A"}
</Code>
<ActionIcon
variant="light"
color="gray"
onClick={() =>
snap.session?.token &&
copyToClipboard(snap.session.token, "token")
}
>
{copied === "token" ? (
<IconCheck size={16} />
) : (
<IconCopy size={16} />
)}
</ActionIcon>
</Group>
</Box>
<Button
variant="light"
color="gray"
fullWidth
leftSection={<IconExternalLink size={16} />}
>
Riwayat Sesi
</Button>
</Stack>
</Card>
</Stack>
</Grid.Col>
</Grid>
</Stack>
</Container>
); );
} }

View File

@@ -0,0 +1,69 @@
import {
AppShell,
Button,
Container,
Group,
Text,
useMantineColorScheme,
} from "@mantine/core";
import { IconChevronLeft } from "@tabler/icons-react";
import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
export const Route = createFileRoute("/profile")({
component: ProfileLayout,
});
function ProfileLayout() {
const navigate = useNavigate();
const { colorScheme } = useMantineColorScheme();
const dark = colorScheme === "dark";
return (
<AppShell
header={{ height: 60 }}
padding="md"
styles={{
main: {
backgroundColor: dark
? "var(--mantine-color-dark-8)"
: "var(--mantine-color-gray-0)",
},
}}
>
<AppShell.Header
style={{
borderBottom: "1px solid var(--mantine-color-default-border)",
backgroundColor: dark ? "var(--mantine-color-dark-7)" : "white",
paddingLeft: "1rem",
paddingRight: "1rem",
}}
>
<Group h="100%" justify="space-between">
<Group>
<Button
variant="subtle"
color="gray"
leftSection={<IconChevronLeft size={16} />}
onClick={() => navigate({ to: "/" })}
>
Kembali ke Dashboard
</Button>
</Group>
<Text fw={700} size="lg" c="orange.6">
PENGATURAN AKUN
</Text>
<Box w={150} />
</Group>
</AppShell.Header>
<AppShell.Main>
<Outlet />
</AppShell.Main>
</AppShell>
);
}
import { Box } from "@mantine/core";

View File

@@ -1,66 +1,6 @@
import { AppShell, Burger, Group, useMantineColorScheme } from "@mantine/core";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Header } from "@/components/header";
import { Sidebar } from "@/components/sidebar";
import SosialPage from "@/components/sosial-page"; import SosialPage from "@/components/sosial-page";
import { useSidebarFullscreen } from "@/hooks/use-sidebar-fullscreen";
export const Route = createFileRoute("/sosial")({ export const Route = createFileRoute("/sosial")({
component: SosialRoute, component: SosialPage,
}); });
function SosialRoute() {
const {
opened,
toggleMobile,
sidebarCollapsed,
toggleSidebar,
handleMainClick,
} = useSidebarFullscreen();
const { colorScheme } = useMantineColorScheme();
const headerBgColor = colorScheme === "dark" ? "#11192D" : "#19355E";
const navbarBgColor = colorScheme === "dark" ? "#11192D" : "white";
const mainBgColor = colorScheme === "dark" ? "#11192D" : "#edf3f8ff";
return (
<AppShell
header={{ height: 60 }}
navbar={{
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: sidebarCollapsed },
}}
padding="md"
>
<AppShell.Header bg={headerBgColor}>
<Group h="100%" px="md">
<Burger
opened={opened}
onClick={toggleMobile}
hiddenFrom="sm"
size="sm"
/>
<Header onSidebarToggle={toggleSidebar} />
</Group>
</AppShell.Header>
<AppShell.Navbar
p="md"
bg={navbarBgColor}
style={{ display: "flex", flexDirection: "column" }}
>
<div style={{ flex: 1, overflowY: "auto" }}>
<Sidebar />
</div>
</AppShell.Navbar>
<AppShell.Main
bg={mainBgColor}
onClick={handleMainClick}
style={{ cursor: sidebarCollapsed ? "default" : "pointer" }}
>
<SosialPage />
</AppShell.Main>
</AppShell>
);
}