diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index bd8901335..fc3fe3a0f 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Inertia\Middleware; class HandleInertiaRequests extends Middleware @@ -42,9 +43,16 @@ public function share(Request $request): array ...parent::share($request), 'name' => config('app.name'), 'quote' => ['message' => trim($message), 'author' => trim($author)], + "response_uuid" => Str::uuid(), 'auth' => [ 'user' => $request->user(), ], + 'flash' => [ + 'success' => fn() => $request->session()->get('success'), + 'error' => fn() => $request->session()->get('error'), + 'info' => fn() => $request->session()->get('info'), + 'warning' => fn() => $request->session()->get('warning'), + ], 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', ]; } diff --git a/package-lock.json b/package-lock.json index 05b3fcae1..ae5b61059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,8 +31,10 @@ "input-otp": "^1.4.2", "laravel-vite-plugin": "^2.0", "lucide-react": "^0.475.0", + "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", "tw-animate-css": "^1.4.0", @@ -5598,6 +5600,16 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -6532,6 +6544,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index c1b86f4ea..0e27fb504 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,10 @@ "input-otp": "^1.4.2", "laravel-vite-plugin": "^2.0", "lucide-react": "^0.475.0", + "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", "tw-animate-css": "^1.4.0", diff --git a/resources/js/components/FlashMessageDisplayer.tsx b/resources/js/components/FlashMessageDisplayer.tsx new file mode 100644 index 000000000..1533c2646 --- /dev/null +++ b/resources/js/components/FlashMessageDisplayer.tsx @@ -0,0 +1,35 @@ +import { usePage } from "@inertiajs/react"; +import React, { useEffect, useRef } from "react"; +import { toast } from "sonner"; +import { Toaster } from "@/components/ui/sonner"; +import { FlashMessages, SharedData } from "@/types"; + +export default function FlashMessageDisplayer() { + const { props } = usePage(); + const flash: FlashMessages = props.flash ?? {}; + const responseUuid = props.response_uuid; + + const lastUuidRef = useRef(""); + + useEffect(() => { + // Avoid duplicate display + if (!responseUuid || lastUuidRef.current === responseUuid) return; + + lastUuidRef.current = responseUuid; + + if (flash.success) { + toast.success(flash.success, { className: "bg-white text-black" }); + } + if (flash.error) { + toast.error(flash.error, { className: "bg-white text-black" }); + } + if (flash.info) { + toast(flash.info, { className: "bg-white text-black" }); + } + if (flash.warning) { + toast.warning(flash.warning, { className: "bg-white text-black" }); + } + }, [responseUuid, flash]); + + return ; +} diff --git a/resources/js/components/ui/sonner.tsx b/resources/js/components/ui/sonner.tsx new file mode 100644 index 000000000..9f46e06d5 --- /dev/null +++ b/resources/js/components/ui/sonner.tsx @@ -0,0 +1,38 @@ +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from "lucide-react" +import { useTheme } from "next-themes" +import { Toaster as Sonner, type ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + "--border-radius": "var(--radius)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster } diff --git a/resources/js/layouts/app-layout.tsx b/resources/js/layouts/app-layout.tsx index 1457c51ee..2b78322b6 100644 --- a/resources/js/layouts/app-layout.tsx +++ b/resources/js/layouts/app-layout.tsx @@ -1,3 +1,4 @@ +import FlashMessageDisplayer from '@/components/FlashMessageDisplayer'; import AppLayoutTemplate from '@/layouts/app/app-sidebar-layout'; import { type BreadcrumbItem } from '@/types'; import { type ReactNode } from 'react'; @@ -7,8 +8,13 @@ interface AppLayoutProps { breadcrumbs?: BreadcrumbItem[]; } -export default ({ children, breadcrumbs, ...props }: AppLayoutProps) => ( - - {children} - -); +export default ({ children, breadcrumbs, ...props }: AppLayoutProps) => { + + + return ( + + {children} + + + ) +}; diff --git a/resources/js/types/index.d.ts b/resources/js/types/index.d.ts index 2f10844c7..73c36bb41 100644 --- a/resources/js/types/index.d.ts +++ b/resources/js/types/index.d.ts @@ -1,6 +1,13 @@ import { InertiaLinkProps } from '@inertiajs/react'; import { LucideIcon } from 'lucide-react'; + +export interface FlashMessages { + success?: string | null; + error?: string | null; + info?: string | null; + warning?: string | null; +} export interface Auth { user: User; } @@ -28,6 +35,8 @@ export interface SharedData { auth: Auth; sidebarOpen: boolean; [key: string]: unknown; + flash?: FlashMessages; + response_uuid: string } export interface User {