diff --git a/app/Exceptions/ErrorToastException.php b/app/Exceptions/ErrorToastException.php new file mode 100644 index 00000000..900a5eb4 --- /dev/null +++ b/app/Exceptions/ErrorToastException.php @@ -0,0 +1,10 @@ +withRouting( @@ -35,17 +37,53 @@ }) ->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 { + // Show standard modal for easier debugging locally + if (app()->isLocal() && $statusCode === 500) { + return $response; + } + // Return JSON response for PrimeVue toast to display, handled by Inertia router event listener + $errorSummary = "$statusCode - $errorTitles[$statusCode]"; + $errorDetail = $errorDetails[$statusCode]; + if (get_class($exception) === ErrorToastException::class) { + $errorSummary = "$statusCode - Error"; + $errorDetail = $exception->getMessage(); + } + return response()->json([ + 'error_summary' => $errorSummary, + 'error_detail' => $errorDetail, + ], $statusCode); + } + } elseif ($statusCode === 419) { return back()->with([ 'flash_warn' => 'The page expired, please try again.', ]); diff --git a/resources/css/tailwind.css b/resources/css/tailwind.css index 7b6606f0..be494fff 100644 --- a/resources/css/tailwind.css +++ b/resources/css/tailwind.css @@ -6,6 +6,7 @@ @source '../../storage/framework/views/*.php'; @source '../../resources/views/**/*.blade.php'; @source '../../resources/js/**/*.vue'; +@source '../../resources/js/theme/*.js'; @custom-variant dark (&:where(.dark, .dark *)); diff --git a/resources/js/app.js b/resources/js/app.js index 187eb474..296bbc58 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -2,18 +2,21 @@ 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'; import { useSiteColorMode } from '@/composables/useSiteColorMode'; import themePreset from '@/theme/noir-preset'; +import globalPt from '@/theme/global-pt'; /* global Ziggy */ const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; @@ -29,7 +32,32 @@ createInertiaApp({ // Site light/dark mode const colorMode = useSiteColorMode({ emitAuto: true }); - const app = createSSRApp({ render: () => h(App, props) }) + // Global Toast component + const Root = { + setup() { + // show error toast instead of standard Inertia modal response + 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, + }); + } + }); + + return () => h('div', [ + h(App, props), + h(Toast, { position: 'bottom-right' }) + ]); + } + }; + + const app = createSSRApp(Root) .use(plugin) .use(ZiggyVue, Ziggy) .use(PrimeVue, { @@ -43,6 +71,7 @@ createInertiaApp({ }, }, }, + pt: globalPt, }) .use(ToastService) .component('InertiaHead', Head) diff --git a/resources/js/layouts/app/HeaderLayout.vue b/resources/js/layouts/app/HeaderLayout.vue index a08e383f..b5abe504 100644 --- a/resources/js/layouts/app/HeaderLayout.vue +++ b/resources/js/layouts/app/HeaderLayout.vue @@ -82,7 +82,6 @@ const toggleMobileUserMenu = (event) => { -
diff --git a/resources/js/layouts/app/SidebarLayout.vue b/resources/js/layouts/app/SidebarLayout.vue index f0372265..da30b6ca 100644 --- a/resources/js/layouts/app/SidebarLayout.vue +++ b/resources/js/layouts/app/SidebarLayout.vue @@ -80,7 +80,6 @@ const toggleMobileUserMenu = (event) => {
- diff --git a/resources/js/pages/Error.vue b/resources/js/pages/Error.vue index 7f322e9b..51067914 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]; }); @@ -33,15 +24,15 @@ const description = computed(() => {