Skip to content

Commit 545a36d

Browse files
authored
Merge pull request #347 from connorabbas/feature/error-toasts
Feature - Error toasts
2 parents ef721da + f6b416d commit 545a36d

File tree

9 files changed

+128
-35
lines changed

9 files changed

+128
-35
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace App\Exceptions;
4+
5+
use Exception;
6+
7+
class ErrorToastException extends Exception
8+
{
9+
//
10+
}

bootstrap/app.php

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php
22

3+
use App\Exceptions\ErrorToastException;
34
use App\Http\Middleware\EncryptCookies;
45
use App\Http\Middleware\HandleInertiaRequests;
56
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncryptCookies;
@@ -10,6 +11,7 @@
1011
use Illuminate\Http\Request;
1112
use Inertia\Inertia;
1213
use Symfony\Component\HttpFoundation\Response;
14+
use Tighten\Ziggy\Ziggy;
1315

1416
return Application::configure(basePath: dirname(__DIR__))
1517
->withRouting(
@@ -30,17 +32,53 @@
3032
})
3133
->withExceptions(function (Exceptions $exceptions) {
3234
$exceptions->respond(function (Response $response, Throwable $exception, Request $request) {
33-
if (
34-
!app()->environment(['local', 'testing'])
35-
&& in_array($response->getStatusCode(), [500, 503, 404, 403])
36-
) {
37-
return Inertia::render('Error', [
38-
'homepageRoute' => route('welcome'),
39-
'status' => $response->getStatusCode()
40-
])
41-
->toResponse($request)
42-
->setStatusCode($response->getStatusCode());
43-
} elseif ($response->getStatusCode() === 419) {
35+
$statusCode = $response->getStatusCode();
36+
$errorTitles = [
37+
403 => 'Forbidden',
38+
404 => 'Not Found',
39+
500 => 'Server Error',
40+
503 => 'Service Unavailable',
41+
];
42+
$errorDetails = [
43+
403 => 'Sorry, you are unauthorized to access this resource/action.',
44+
404 => 'Sorry, the resource you are looking for could not be found.',
45+
500 => 'Whoops, something went wrong on our end. Please try again.',
46+
503 => 'Sorry, we are doing some maintenance. Please check back soon.',
47+
];
48+
49+
if (in_array($statusCode, [500, 503, 404, 403])) {
50+
if (!$request->inertia()) {
51+
// Show error page component for standard visits
52+
return Inertia::render('Error', [
53+
'errorTitles' => $errorTitles,
54+
'errorDetails' => $errorDetails,
55+
'status' => $statusCode,
56+
'homepageRoute' => route('welcome'),
57+
'ziggy' => fn () => [
58+
...(new Ziggy())->toArray(),
59+
'location' => $request->url(),
60+
],
61+
])
62+
->toResponse($request)
63+
->setStatusCode($statusCode);
64+
} else {
65+
// Show standard modal for easier debugging locally
66+
if (app()->isLocal() && $statusCode === 500) {
67+
return $response;
68+
}
69+
// Return JSON response for PrimeVue toast to display, handled by Inertia router event listener
70+
$errorSummary = "$statusCode - $errorTitles[$statusCode]";
71+
$errorDetail = $errorDetails[$statusCode];
72+
if (get_class($exception) === ErrorToastException::class) {
73+
$errorSummary = "$statusCode - Error";
74+
$errorDetail = $exception->getMessage();
75+
}
76+
return response()->json([
77+
'error_summary' => $errorSummary,
78+
'error_detail' => $errorDetail,
79+
], $statusCode);
80+
}
81+
} elseif ($statusCode === 419) {
4482
return back()->with([
4583
'flash_warn' => 'The page expired, please try again.',
4684
]);

resources/css/tailwind.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
@source '../../storage/framework/views/*.php';
77
@source '../../resources/views/**/*.blade.php';
88
@source '../../resources/js/**/*.vue';
9+
@source '../../resources/js/theme/*.js';
910

1011
@custom-variant dark (&:where(.dark, .dark *));
1112

resources/js/app.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,21 @@ import '../css/app.css';
22
import '../css/tailwind.css';
33

44
import { createSSRApp, h } from 'vue';
5-
import { createInertiaApp, Head, Link } from '@inertiajs/vue3';
5+
import { createInertiaApp, router, Head, Link } from '@inertiajs/vue3';
66
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
77
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
88

99
import PrimeVue from 'primevue/config';
1010
import ToastService from 'primevue/toastservice';
11+
import { useToast } from 'primevue/usetoast';
12+
import Toast from 'primevue/toast';
1113

1214
import Container from '@/components/Container.vue';
1315
import PageTitleSection from '@/components/PageTitleSection.vue';
1416

1517
import { useSiteColorMode } from '@/composables/useSiteColorMode';
1618
import themePreset from '@/theme/noir-preset';
19+
import globalPt from '@/theme/global-pt';
1720

1821
/* global Ziggy */
1922
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
@@ -29,7 +32,32 @@ createInertiaApp({
2932
// Site light/dark mode
3033
const colorMode = useSiteColorMode({ emitAuto: true });
3134

32-
const app = createSSRApp({ render: () => h(App, props) })
35+
// Global Toast component
36+
const Root = {
37+
setup() {
38+
// show error toast instead of standard Inertia modal response
39+
const toast = useToast();
40+
router.on('invalid', (event) => {
41+
const responseBody = event.detail.response?.data;
42+
if (responseBody?.error_summary && responseBody?.error_detail) {
43+
event.preventDefault();
44+
toast.add({
45+
severity: event.detail.response?.status >= 500 ? 'error' : 'warn',
46+
summary: responseBody.error_summary,
47+
detail: responseBody.error_detail,
48+
life: 5000,
49+
});
50+
}
51+
});
52+
53+
return () => h('div', [
54+
h(App, props),
55+
h(Toast, { position: 'bottom-right' })
56+
]);
57+
}
58+
};
59+
60+
const app = createSSRApp(Root)
3361
.use(plugin)
3462
.use(ZiggyVue, Ziggy)
3563
.use(PrimeVue, {
@@ -43,6 +71,7 @@ createInertiaApp({
4371
},
4472
},
4573
},
74+
pt: globalPt,
4675
})
4776
.use(ToastService)
4877
.component('InertiaHead', Head)

resources/js/layouts/app/HeaderLayout.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ const toggleMobileUserMenu = (event) => {
7676
</div>
7777
</template>
7878
</Drawer>
79-
<Toast position="top-center" />
8079
</Teleport>
8180
</ClientOnly>
8281
<div class="min-h-screen">

resources/js/layouts/app/SidebarLayout.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ const toggleMobileUserMenu = (event) => {
7474
</div>
7575
</template>
7676
</Drawer>
77-
<Toast position="top-center" />
7877
</Teleport>
7978
</ClientOnly>
8079

resources/js/pages/Error.vue

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,17 @@ import { computed } from 'vue';
33
import { ArrowLeft } from 'lucide-vue-next';
44
55
const props = defineProps({
6+
errorTitles: Object,
7+
errorDetails: Object,
8+
status: Number,
69
homepageRoute: String,
7-
status: Number
810
});
911
1012
const title = computed(() => {
11-
return {
12-
503: 'Service Unavailable',
13-
500: 'Server Error',
14-
404: 'Page Not Found',
15-
403: 'Forbidden',
16-
}[props.status];
13+
return props.errorTitles[props.status];
1714
});
18-
19-
const description = computed(() => {
20-
return {
21-
503: 'Sorry, we are doing some maintenance. Please check back soon.',
22-
500: 'Whoops, something went wrong on our servers.',
23-
404: 'Sorry, the page you are looking for could not be found.',
24-
403: 'Sorry, you are forbidden from accessing this page.',
25-
}[props.status];
15+
const details = computed(() => {
16+
return props.errorDetails[props.status];
2617
});
2718
</script>
2819

@@ -33,15 +24,15 @@ const description = computed(() => {
3324
<div class="h-screen flex items-center justify-center">
3425
<Card class="p-4 py-6 sm:p-12">
3526
<template #content>
36-
<div class="flex flex-col gap-8 items-center justify-center text-center">
37-
<h1 class="font-extrabold text-5xl md:text-8xl text-primary">
27+
<div class="flex flex-col gap-6 md:gap-8 items-center justify-center text-center">
28+
<h1 class="font-extrabold text-2xl md:text-4xl text-primary">
3829
{{ props.status }}
3930
</h1>
4031
<h2 class="font-extrabold text-4xl md:text-6xl">
4132
{{ title }}
4233
</h2>
43-
<p class="text-xl font-semibold md:text-3xl text-muted-color">
44-
{{ description }}
34+
<p class="text-xl font-semibold text-muted-color">
35+
{{ details }}
4536
</p>
4637
<InertiaLink :href="props.homepageRoute">
4738
<Button

resources/js/ssr.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { route as ziggyRoute } from 'ziggy-js';
99

1010
import PrimeVue from 'primevue/config';
1111
import ToastService from 'primevue/toastservice';
12+
import Toast from 'primevue/toast';
1213

1314
import Container from '@/components/Container.vue';
1415
import PageTitleSection from '@/components/PageTitleSection.vue';
@@ -33,8 +34,18 @@ createServer((page) =>
3334
emitAuto: true,
3435
});
3536

37+
// Global Toast component
38+
const Root = {
39+
setup() {
40+
return () => h('div', [
41+
h(App, props),
42+
h(Toast, { position: 'bottom-right' })
43+
]);
44+
}
45+
};
46+
3647
// Create app
37-
const app = createSSRApp({ render: () => h(App, props) });
48+
const app = createSSRApp(Root);
3849

3950
// Configure Ziggy for SSR
4051
const ziggyConfig = {

resources/js/theme/global-pt.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Global pass through styling for components
3+
* https://primevue.org/passthrough/#global
4+
*/
5+
export default {
6+
toast: {
7+
root: {
8+
// Full width/centered on mobile, bottom right desktop
9+
class: 'fixed! left-4! right-4! bottom-4! w-auto! md:right-8! md:bottom-8! sm:w-[25rem]! sm:not-fixed! sm:left-auto! sm:ml-auto!'
10+
},
11+
message: {
12+
class: 'shadow-lg mb-0 mt-4'
13+
},
14+
},
15+
};

0 commit comments

Comments
 (0)