feat: add custom error page handler and improve permission check safety in sidebar navigation

This commit is contained in:
Yoga Pangestu 2026-04-30 15:27:07 +07:00
parent 1368ed0b54
commit eb13532b0b
4 changed files with 101 additions and 3 deletions

View File

@ -7,6 +7,7 @@
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets; use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
use Inertia\Inertia;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -27,5 +28,13 @@
$schedule->command('payroll:generate')->monthlyOn(1, '00:00'); $schedule->command('payroll:generate')->monthlyOn(1, '00:00');
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// $exceptions->respond(function ($response) {
if (in_array($response->getStatusCode(), [500, 503, 404, 403])) {
return Inertia::render('error', ['status' => $response->getStatusCode()])
->toResponse(request())
->setStatusCode($response->getStatusCode());
}
return $response;
});
})->create(); })->create();

View File

@ -14,7 +14,7 @@ createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName), title: (title) => (title ? `${title} - ${appName}` : appName),
layout: (name) => { layout: (name) => {
switch (true) { switch (true) {
case name === 'welcome' || name === 'homepage': case name === 'welcome' || name === 'homepage' || name === 'error':
return null; return null;
case name.startsWith('auth/'): case name.startsWith('auth/'):
return AuthLayout; return AuthLayout;

View File

@ -137,7 +137,7 @@ export function AppSidebar() {
const filterNavItems = (items: NavItem[]) => { const filterNavItems = (items: NavItem[]) => {
return items.filter((item) => { return items.filter((item) => {
if (!item.permission) return true; if (!item.permission) return true;
return auth.permissions.includes(item.permission); return auth?.permissions?.includes(item.permission) ?? false;
}); });
}; };

View File

@ -0,0 +1,89 @@
import { Button } from '@/components/ui/button';
import { homepage } from '@/routes';
import { Head, Link } from '@inertiajs/react';
import { AlertCircle, ArrowLeft, Home, Search } from 'lucide-react';
export default function ErrorPage({ status }: { status: number }) {
const title =
{
503: '503: Service Unavailable',
500: '500: Server Error',
404: '404: Page Not Found',
403: '403: Forbidden',
}[status] || 'Error';
const description =
{
503: 'Maaf, kami sedang melakukan pemeliharaan rutin untuk meningkatkan layanan. Silakan kembali lagi nanti.',
500: 'Ups, terjadi kesalahan sistem yang tidak terduga. Tim teknis kami telah diberitahu.',
404: 'Halaman yang Anda cari tidak dapat ditemukan atau telah dipindahkan ke alamat lain.',
403: 'Maaf, Anda tidak memiliki akses untuk melihat konten di halaman ini.',
}[status] || 'Terjadi kesalahan pada aplikasi.';
return (
<>
<Head title={title} />
<div className="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-slate-50 p-6 dark:bg-slate-950">
{/* Abstract Background Elements */}
<div className="absolute -top-[10%] -left-[10%] h-[40%] w-[40%] rounded-full bg-primary/5 blur-3xl" />
<div className="absolute -right-[10%] -bottom-[10%] h-[40%] w-[40%] rounded-full bg-primary/10 blur-3xl" />
<div className="relative z-10 flex w-full max-w-2xl flex-col items-center text-center">
{/* Big Error Number with Gradient */}
<div className="animate-in duration-700 fade-in zoom-in">
<h1 className="text-[12rem] leading-none font-black tracking-tighter text-slate-200/50 select-none sm:text-[18rem] dark:text-slate-800/50">
{status}
</h1>
</div>
{/* Content Card */}
<div className="mt-[-4rem] flex flex-col items-center space-y-8 sm:mt-[-8rem]">
<div className="relative">
<div className="absolute -inset-1 rounded-full bg-gradient-to-r from-primary to-primary-foreground opacity-20 blur" />
<div className="relative flex h-24 w-24 items-center justify-center rounded-full bg-white shadow-2xl dark:bg-slate-900">
{status === 404 ? (
<Search className="h-10 w-10 animate-pulse text-primary" />
) : (
<AlertCircle className="h-10 w-10 animate-bounce text-destructive" />
)}
</div>
</div>
<div className="space-y-3 px-4">
<h2 className="text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl dark:text-slate-50">
{title}
</h2>
<p className="mx-auto max-w-md text-lg text-slate-600 dark:text-slate-400">
{description}
</p>
</div>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<Button
asChild
size="lg"
className="h-12 gap-2 px-8 shadow-lg shadow-primary/20 transition-all hover:scale-105 active:scale-95"
>
<Link href={homepage().url}>
<Home className="h-4 w-4" />
Beranda
</Link>
</Button>
<Button
variant="outline"
size="lg"
onClick={() => window.history.back()}
className="h-12 gap-2 bg-white/50 backdrop-blur-sm transition-all hover:bg-white dark:bg-slate-900/50 dark:hover:bg-slate-900"
>
<ArrowLeft className="h-4 w-4" />
Kembali
</Button>
</div>
</div>
</div>
</div>
</>
);
}
ErrorPage.layout = null;