refactor: consolidate individual system pages into a single reusable StatusPage component

This commit is contained in:
Firman Ramdhani
2026-08-03 11:20:45 +07:00
parent bfea7e7773
commit 9e30c8a5e5
9 changed files with 174 additions and 172 deletions
+60 -5
View File
@@ -1,7 +1,8 @@
import { lazy, Suspense, useEffect } from 'react'; import { lazy, Suspense, useEffect } from 'react';
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom'; import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import { ThemeProvider } from '@repo/ui/provider'; import { ThemeProvider } from '@repo/ui/provider';
import { NotFound, Forbidden, Maintenance, ComingSoon, AgGridProvider } from '@repo/ui/components'; import { StatusPage, AgGridProvider } from '@repo/ui/components';
import { useTranslation } from '@repo/core-i18n';
import { LoadingScreen } from '../core/components/loading-screen'; import { LoadingScreen } from '../core/components/loading-screen';
import { useThemeStore } from '../core/stores/theme.store'; import { useThemeStore } from '../core/stores/theme.store';
import { initializeAndPurgeHistoryBackground } from './main/layouts/hooks/useHistoryTracker'; import { initializeAndPurgeHistoryBackground } from './main/layouts/hooks/useHistoryTracker';
@@ -9,13 +10,67 @@ import { initializeAndPurgeHistoryBackground } from './main/layouts/hooks/useHis
const AuthModule = lazy(() => import('./auth')); const AuthModule = lazy(() => import('./auth'));
const AppModule = lazy(() => import('./main')); const AppModule = lazy(() => import('./main'));
function NotFoundPage() {
const { t } = useTranslation();
return (
<StatusPage
title={t('common:systemPages.notFound.title')}
heading={t('common:systemPages.notFound.heading')}
description={t('common:systemPages.notFound.description')}
showActionsBack
showActionsHome
homeUrl="/app"
backButtonLabel={t('common:systemPages.actions.goBack')}
homeButtonLabel={t('common:systemPages.actions.backToHome')}
/>
);
}
function ForbiddenPage() {
const { t } = useTranslation();
return (
<StatusPage
title={t('common:systemPages.forbidden.title')}
heading={t('common:systemPages.forbidden.heading')}
description={t('common:systemPages.forbidden.description')}
showActionsBack
showActionsHome
homeUrl="/app"
backButtonLabel={t('common:systemPages.actions.goBack')}
homeButtonLabel={t('common:systemPages.actions.backToHome')}
/>
);
}
function MaintenancePage() {
const { t } = useTranslation();
return (
<StatusPage
title={t('common:systemPages.maintenance.title')}
heading={t('common:systemPages.maintenance.heading')}
description={t('common:systemPages.maintenance.description')}
/>
);
}
function ComingSoonPage() {
const { t } = useTranslation();
return (
<StatusPage
title={t('common:systemPages.comingSoon.title')}
heading={t('common:systemPages.comingSoon.heading')}
description={t('common:systemPages.comingSoon.description')}
/>
);
}
const router = createBrowserRouter([ const router = createBrowserRouter([
{ path: '/auth/*', element: <AuthModule /> }, { path: '/auth/*', element: <AuthModule /> },
{ path: '/app/*', element: <AppModule /> }, { path: '/app/*', element: <AppModule /> },
{ path: '/404', element: <NotFound homeUrl="/app" /> }, { path: '/404', element: <NotFoundPage /> },
{ path: '/403', element: <Forbidden homeUrl="/app" /> }, { path: '/403', element: <ForbiddenPage /> },
{ path: '/maintenance', element: <Maintenance /> }, { path: '/maintenance', element: <MaintenancePage /> },
{ path: '/coming-soon', element: <ComingSoon /> }, { path: '/coming-soon', element: <ComingSoonPage /> },
{ path: '/', element: <Navigate to="/app" /> }, { path: '/', element: <Navigate to="/app" /> },
{ path: '*', element: <Navigate to="/404" /> }, { path: '*', element: <Navigate to="/404" /> },
]); ]);
@@ -195,6 +195,32 @@
"endDate": "End Date", "endDate": "End Date",
"attachment": "Attachment", "attachment": "Attachment",
"reference": "Reference" "reference": "Reference"
},
"systemPages": {
"comingSoon": {
"title": "Coming Soon",
"heading": "Feature in Progress",
"description": "This feature is currently under development. Stay tuned! We'll have it ready for you very soon."
},
"forbidden": {
"title": "403 Error",
"heading": "Access Denied",
"description": "Sorry, you dont have permission to access this page. Please contact your administrator if you believe this is a mistake."
},
"maintenance": {
"title": "Maintenance",
"heading": "We'll Be Back Soon",
"description": "Our system is currently undergoing scheduled maintenance. We are working hard to bring it back online as soon as possible."
},
"notFound": {
"title": "404 Error",
"heading": "Page Not Found",
"description": "Sorry, the page youre looking for doesnt exist or has been moved."
},
"actions": {
"backToHome": "Back to Home",
"goBack": "Go Back"
}
} }
} }
} }
@@ -195,6 +195,32 @@
"endDate": "Tanggal Selesai", "endDate": "Tanggal Selesai",
"attachment": "Lampiran", "attachment": "Lampiran",
"reference": "Referensi" "reference": "Referensi"
},
"systemPages": {
"comingSoon": {
"title": "Segera Hadir",
"heading": "Fitur dalam Pengembangan",
"description": "Fitur ini sedang dalam tahap pengembangan. Tetap ikuti pembaruannya! Kami akan segera menyiapkannya untuk Anda."
},
"forbidden": {
"title": "Error 403",
"heading": "Akses Ditolak",
"description": "Maaf, Anda tidak memiliki izin untuk mengakses halaman ini. Silakan hubungi administrator jika Anda merasa ini adalah kesalahan."
},
"maintenance": {
"title": "Pemeliharaan",
"heading": "Kami Akan Segera Kembali",
"description": "Sistem kami saat ini sedang menjalani pemeliharaan terjadwal. Kami bekerja keras untuk menghadirkannya kembali sesegera mungkin."
},
"notFound": {
"title": "Error 404",
"heading": "Halaman Tidak Ditemukan",
"description": "Maaf, halaman yang Anda cari tidak ada atau telah dipindahkan."
},
"actions": {
"backToHome": "Kembali ke Beranda",
"goBack": "Kembali"
}
} }
} }
} }
+1 -4
View File
@@ -2,10 +2,7 @@ export * from '@mantine/core';
export { List, TypographyStylesProvider } from '@mantine/core'; export { List, TypographyStylesProvider } from '@mantine/core';
export * from './Form'; export * from './Form';
export * from './system-pages/coming-soon'; export * from './system-pages/status-page';
export * from './system-pages/forbidden';
export * from './system-pages/maintenance';
export * from './system-pages/not-found';
export * from './core-app-shell'; export * from './core-app-shell';
export * from './actions-tools'; export * from './actions-tools';
export * from './status-badge'; export * from './status-badge';
@@ -1,34 +0,0 @@
interface ComingSoonProps {
showActions?: boolean;
height?: StyleProp<React.CSSProperties['height']>;
}
import { Title, Text, Button, Container, Stack, Center, StyleProp } from '@mantine/core';
export function ComingSoon({ showActions = false, height = '100vh' }: ComingSoonProps) {
return (
<Container size="sm" h={height} pos="relative">
<Center h="100%">
<Stack align="center" ta="center" gap="md">
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
Coming Soon
</Text>
<Title order={1} fw={900} c="brand">
Feature in Progress
</Title>
<Text size="md" c="dimmed">
This feature is currently under development. Stay tuned! We'll have it ready for you very soon.
</Text>
{showActions && (
<Button component="a" href="/" color="brand" size="sm" mt="xl">
Back to Home
</Button>
)}
</Stack>
</Center>
</Container>
);
}
@@ -1,68 +0,0 @@
interface ForbiddenProps {
showActionsBack?: boolean;
showActionsHome?: boolean;
onClickGoBack?(): void;
onClickBackToHome?(): void;
homeUrl?: string;
height?: StyleProp<React.CSSProperties['height']>;
}
import { Title, Text, Button, Container, Stack, Group, Center, StyleProp } from '@mantine/core';
import { useNavigate } from 'react-router-dom';
export function Forbidden({
showActionsBack = true,
showActionsHome = true,
onClickGoBack,
onClickBackToHome,
homeUrl,
height = '100vh',
}: ForbiddenProps) {
const navigate = useNavigate();
function onBack() {
if (onClickGoBack) onClickGoBack();
else navigate(-1);
}
function onBackHome() {
if (onClickBackToHome) onClickBackToHome();
else if (homeUrl) navigate(homeUrl, { replace: true });
}
return (
<Container size="sm" h={height} pos="relative">
<Center h="100%">
<Stack align="center" ta="center" gap="md">
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
403 Error
</Text>
<Title order={1} fw={900} c="brand">
Access Denied
</Title>
<Text size="md" c="dimmed">
Sorry, you dont have permission to access this page. Please contact your administrator if you believe this
is a mistake.
</Text>
{(showActionsBack || showActionsHome) && (
<Group mt="xl" justify="center">
{showActionsBack && (
<Button variant="default" size="sm" onClick={onBack}>
Go Back
</Button>
)}
{showActionsHome && (
<Button color="brand" size="sm" onClick={onBackHome}>
Back to Home
</Button>
)}
</Group>
)}
</Stack>
</Center>
</Container>
);
}
@@ -1,35 +0,0 @@
interface MaintenanceProps {
showActions?: boolean;
height?: StyleProp<React.CSSProperties['height']>;
}
import { Title, Text, Button, Container, Stack, Center, StyleProp } from '@mantine/core';
export function Maintenance({ showActions = false, height = '100vh' }: MaintenanceProps) {
return (
<Container size="sm" h={height} pos="relative">
<Center h="100%">
<Stack align="center" ta="center" gap="md">
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
Maintenance
</Text>
<Title order={1} fw={900} c="brand">
We'll Be Back Soon
</Title>
<Text size="md" c="dimmed">
Our system is currently undergoing scheduled maintenance. We are working hard to bring it back online as
soon as possible.
</Text>
{showActions && (
<Button component="a" href="/" color="brand" size="sm" mt="xl">
Back to Home
</Button>
)}
</Stack>
</Center>
</Container>
);
}
@@ -1,23 +1,38 @@
interface NotFoundProps {
showActionsBack?: boolean;
showActionsHome?: boolean;
onClickGoBack?(): void;
onClickBackToHome?(): void;
homeUrl?: string;
height?: StyleProp<React.CSSProperties['height']>;
}
import { Title, Text, Button, Container, Stack, Group, Center, StyleProp } from '@mantine/core'; import { Title, Text, Button, Container, Stack, Group, Center, StyleProp } from '@mantine/core';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
export function NotFound({ export interface StatusPageProps {
showActionsBack = true, title?: string;
showActionsHome = true, heading?: string;
description?: string;
icon?: React.ReactNode;
showActionsBack?: boolean;
backButtonLabel?: string;
onClickGoBack?(): void;
showActionsHome?: boolean;
homeButtonLabel?: string;
onClickBackToHome?(): void;
homeUrl?: string;
height?: StyleProp<React.CSSProperties['height']>;
}
export function StatusPage({
title,
heading,
description,
icon,
showActionsBack = false,
backButtonLabel,
onClickGoBack, onClickGoBack,
showActionsHome = false,
homeButtonLabel,
onClickBackToHome, onClickBackToHome,
homeUrl, homeUrl,
height = '100vh', height = '100vh',
}: NotFoundProps) { }: StatusPageProps) {
const navigate = useNavigate(); const navigate = useNavigate();
function onBack() { function onBack() {
@@ -34,29 +49,37 @@ export function NotFound({
<Container size="sm" h={height} pos="relative"> <Container size="sm" h={height} pos="relative">
<Center h="100%"> <Center h="100%">
<Stack align="center" ta="center" gap="md"> <Stack align="center" ta="center" gap="md">
{icon && icon}
{title && (
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)"> <Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
404 Error {title}
</Text> </Text>
)}
{heading && (
<Title order={1} fw={900} c="brand"> <Title order={1} fw={900} c="brand">
Page Not Found {heading}
</Title> </Title>
)}
{description && (
<Text size="md" c="dimmed"> <Text size="md" c="dimmed">
Sorry, the page youre looking for doesnt exist or has been moved. {description}
</Text> </Text>
)}
{(showActionsBack || showActionsHome) && ( {(showActionsBack || showActionsHome) && (
<Group mt="xl" justify="center"> <Group mt="xl" justify="center">
{showActionsBack && ( {showActionsBack && (
<Button variant="default" size="sm" onClick={onBack}> <Button variant="default" size="sm" onClick={onBack}>
Go Back {backButtonLabel || 'Go Back'}
</Button> </Button>
)} )}
{showActionsHome && ( {showActionsHome && (
<Button color="brand" size="sm" onClick={onBackHome}> <Button color="brand" size="sm" onClick={onBackHome}>
Back to Home {homeButtonLabel || 'Back to Home'}
</Button> </Button>
)} )}
</Group> </Group>
@@ -19,7 +19,7 @@ import {
EnterpriseModalContext, EnterpriseModalContext,
EnterpriseTranslationContext, EnterpriseTranslationContext,
} from '../hooks/use-module.context'; } from '../hooks/use-module.context';
import { Forbidden } from '../../../components'; import { StatusPage } from '../../../components';
import { useEnterpriseStorage } from '../hooks/enterprise-storage.context'; import { useEnterpriseStorage } from '../hooks/enterprise-storage.context';
import { defaultPrivileges, noPrivileges } from '../constant'; import { defaultPrivileges, noPrivileges } from '../constant';
@@ -176,7 +176,19 @@ export function EnterpriseModuleProvider<
const { ALLOW_VIEW } = configSlice.privileges ?? {}; const { ALLOW_VIEW } = configSlice.privileges ?? {};
if (!ALLOW_VIEW) return <Forbidden homeUrl="/app" height={500} />; if (!ALLOW_VIEW) {
return (
<StatusPage
title={t('common:systemPages.forbidden.title')}
heading={t('common:systemPages.forbidden.heading')}
description={t('common:systemPages.forbidden.description')}
showActionsHome
homeUrl="/app"
homeButtonLabel={t('common:systemPages.actions.backToHome')}
height={500}
/>
);
}
return ( return (
<EnterpriseConfigContext.Provider value={configSlice}> <EnterpriseConfigContext.Provider value={configSlice}>