From a406a508e52236cb20ac76af6e8ae4ecf07cf6b9 Mon Sep 17 00:00:00 2001 From: Connor Abbas Date: Thu, 12 Jun 2025 02:11:55 +0000 Subject: [PATCH 01/10] feature: handle inertia response errors with Toast instead of default modal --- bootstrap/app.php | 53 +++++++++++++++++----- resources/js/app.js | 30 +++++++++++- resources/js/layouts/app/HeaderLayout.vue | 1 - resources/js/layouts/app/SidebarLayout.vue | 1 - resources/js/pages/Error.vue | 23 +++------- resources/js/ssr.js | 13 +++++- 6 files changed, 89 insertions(+), 32 deletions(-) diff --git a/bootstrap/app.php b/bootstrap/app.php index ddf48a84..a81be7b3 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -10,6 +10,7 @@ use Illuminate\Http\Request; use Inertia\Inertia; use Symfony\Component\HttpFoundation\Response; +use Tighten\Ziggy\Ziggy; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -30,17 +31,47 @@ }) ->withExceptions(function (Exceptions $exceptions) { $exceptions->respond(function (Response $response, Throwable $exception, Request $request) { - if ( - !app()->environment(['local', 'testing']) - && in_array($response->getStatusCode(), [500, 503, 404, 403]) - ) { - return Inertia::render('Error', [ - 'homepageRoute' => route('welcome'), - 'status' => $response->getStatusCode() - ]) - ->toResponse($request) - ->setStatusCode($response->getStatusCode()); - } elseif ($response->getStatusCode() === 419) { + $statusCode = $response->getStatusCode(); + $errorTitles = [ + 403 => 'Forbidden', + 404 => 'Not Found', + 500 => 'Server Error', + 503 => 'Service Unavailable', + ]; + $errorDetails = [ + 403 => 'Sorry, you are unauthorized to access this resource/action.', + 404 => 'Sorry, the resource you are looking for could not be found.', + 500 => 'Whoops, something went wrong on our end. Please try again.', + 503 => 'Sorry, we are doing some maintenance. Please check back soon.', + ]; + + if (in_array($statusCode, [500, 503, 404, 403])) { + if (!$request->inertia()) { + // Show error page component for standard visits + return Inertia::render('Error', [ + 'errorTitles' => $errorTitles, + 'errorDetails' => $errorDetails, + 'status' => $statusCode, + 'homepageRoute' => route('welcome'), + 'ziggy' => fn () => [ + ...(new Ziggy())->toArray(), + 'location' => $request->url(), + ], + ]) + ->toResponse($request) + ->setStatusCode($statusCode); + } else { + // Return JSON response for PrimeVue toast to display + $exceptionMessage = ''; + if (app()->isLocal()) { + $exceptionMessage = sprintf("\n\n%s: %s", get_class($exception), $exception->getMessage()); + } + return response()->json([ + 'error_summary' => "$statusCode - $errorTitles[$statusCode]", + 'error_detail' => $errorDetails[$statusCode] . $exceptionMessage, + ], $statusCode); + } + } elseif ($statusCode === 419) { return back()->with([ 'flash_message' => 'The page expired, please try again.', ]); diff --git a/resources/js/app.js b/resources/js/app.js index 187eb474..6db73420 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -2,12 +2,14 @@ import '../css/app.css'; import '../css/tailwind.css'; import { createSSRApp, h } from 'vue'; -import { createInertiaApp, Head, Link } from '@inertiajs/vue3'; +import { createInertiaApp, router, Head, Link } from '@inertiajs/vue3'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { ZiggyVue } from '../../vendor/tightenco/ziggy'; import PrimeVue from 'primevue/config'; import ToastService from 'primevue/toastservice'; +import { useToast } from 'primevue/usetoast'; +import Toast from 'primevue/toast'; import Container from '@/components/Container.vue'; import PageTitleSection from '@/components/PageTitleSection.vue'; @@ -29,7 +31,31 @@ createInertiaApp({ // Site light/dark mode const colorMode = useSiteColorMode({ emitAuto: true }); - const app = createSSRApp({ render: () => h(App, props) }) + // Global Toast component, show errors instead of standard Inertia modal response + const Root = { + setup() { + const toast = useToast() + router.on('invalid', (event) => { + const responseBody = event.detail.response?.data; + if (responseBody?.error_summary && responseBody?.error_detail) { + event.preventDefault(); + toast.add({ + severity: event.detail.response?.status >= 500 ? 'error' : 'warn', + summary: responseBody.error_summary, + detail: responseBody.error_detail, + life: 5000, + styleClass: 'mb-0 mt-4', + }); + } + }); + return () => h('div', [ + h(App, props), + h(Toast, { position: 'bottom-right' }) + ]); + } + } + + const app = createSSRApp(Root) .use(plugin) .use(ZiggyVue, Ziggy) .use(PrimeVue, { diff --git a/resources/js/layouts/app/HeaderLayout.vue b/resources/js/layouts/app/HeaderLayout.vue index 145eda3d..1044bea8 100644 --- a/resources/js/layouts/app/HeaderLayout.vue +++ b/resources/js/layouts/app/HeaderLayout.vue @@ -76,7 +76,6 @@ const toggleMobileUserMenu = (event) => { -
diff --git a/resources/js/layouts/app/SidebarLayout.vue b/resources/js/layouts/app/SidebarLayout.vue index a37533c6..ce88e644 100644 --- a/resources/js/layouts/app/SidebarLayout.vue +++ b/resources/js/layouts/app/SidebarLayout.vue @@ -74,7 +74,6 @@ const toggleMobileUserMenu = (event) => {
- diff --git a/resources/js/pages/Error.vue b/resources/js/pages/Error.vue index 7f322e9b..b1fa8882 100644 --- a/resources/js/pages/Error.vue +++ b/resources/js/pages/Error.vue @@ -3,26 +3,17 @@ import { computed } from 'vue'; import { ArrowLeft } from 'lucide-vue-next'; const props = defineProps({ + errorTitles: Object, + errorDetails: Object, + status: Number, homepageRoute: String, - status: Number }); const title = computed(() => { - return { - 503: 'Service Unavailable', - 500: 'Server Error', - 404: 'Page Not Found', - 403: 'Forbidden', - }[props.status]; + return props.errorTitles[props.status]; }); - -const description = computed(() => { - return { - 503: 'Sorry, we are doing some maintenance. Please check back soon.', - 500: 'Whoops, something went wrong on our servers.', - 404: 'Sorry, the page you are looking for could not be found.', - 403: 'Sorry, you are forbidden from accessing this page.', - }[props.status]; +const details = computed(() => { + return props.errorDetails[props.status]; }); @@ -41,7 +32,7 @@ const description = computed(() => { {{ title }}

- {{ description }} + {{ details }}