diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx
index c1dc0ae..41284ce 100644
--- a/app/(app)/layout.tsx
+++ b/app/(app)/layout.tsx
@@ -1,6 +1,6 @@
/**
* @file layout.tsx
- * @description Server component layout protecting authenticated routes, verifying user session status, and rendering the responsive application shell structure.
+ * @description Server component layout protecting authenticated routes, verifying user session status, and rendering the responsive application shell structure with global toast notifications.
*/
import { auth } from "@/auth";
@@ -8,6 +8,7 @@ import { redirect } from "next/navigation";
import Sidebar from "@/app/components/layout/Sidebar";
import Header from "@/app/components/layout/Header";
import Navbar from "@/app/components/layout/Navbar";
+import ErrorToast from "../components/ErrorToast";
/**
* Renders the protected application layout wrapper.
@@ -31,6 +32,7 @@ export default async function AppLayout({
return (
<>
+
diff --git a/app/(app)/tasks/page.tsx b/app/(app)/tasks/page.tsx
index 1c70b38..9a22ff5 100644
--- a/app/(app)/tasks/page.tsx
+++ b/app/(app)/tasks/page.tsx
@@ -43,11 +43,12 @@ export default async function TaskPage({ searchParams }: TaskPageProps) {
let initialData = undefined;
if (params.task === "edit" && params.id) {
- if (!UUID_REGEX.test(params.id)) redirect("/dashboard");
+ if (!UUID_REGEX.test(params.id))
+ redirect("/dashboard?error=task_not_found");
initialData = await TaskService.getEditableTask(params.id, session.user.id);
- if (!initialData) redirect("/dashboard");
+ if (!initialData) redirect("/dashboard?error=unauthorized_edit");
}
const users = await db
diff --git a/app/components/ErrorToast.tsx b/app/components/ErrorToast.tsx
new file mode 100644
index 0000000..270e114
--- /dev/null
+++ b/app/components/ErrorToast.tsx
@@ -0,0 +1,138 @@
+/**
+ * @file components/ErrorToast.tsx
+ * @description Client component displaying a modern floating glassmorphism error notification with a pauseable countdown progress bar driven by requestAnimationFrame.
+ */
+
+"use client";
+
+import { useSearchParams, useRouter } from "next/navigation";
+import { useEffect, useState, useRef, useCallback } from "react";
+import { AlertCircle, X } from "lucide-react";
+
+const AUTO_DISMISS_TIME = 5000;
+
+/**
+ * Renders an optimized, pauseable error toast notification using requestAnimationFrame for smooth progress animations,
+ * automatically dismissing itself or handling user-triggered closures.
+ *
+ * @returns {JSX.Element | null} The rendered error toast component or null if not visible.
+ */
+export default function ErrorToast() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const error = searchParams.get("error");
+
+ const [isVisible, setIsVisible] = useState(false);
+ const [progress, setProgress] = useState(100);
+ const [isPaused, setIsPaused] = useState(false);
+
+ const timeLeftRef = useRef(AUTO_DISMISS_TIME);
+ const prevTimeRef = useRef(null);
+ const animFrameRef = useRef(null);
+
+ /**
+ * Dismisses the toast notification and cleans up URL parameters.
+ */
+ const dismissToast = useCallback(() => {
+ setIsVisible(false);
+ router.replace("/dashboard", { scroll: false });
+ }, [router]);
+
+ // Show a toast message when an error occurs and reset the timer
+ useEffect(() => {
+ if (error) {
+ setIsVisible(true);
+ setProgress(100);
+ timeLeftRef.current = AUTO_DISMISS_TIME;
+ prevTimeRef.current = null;
+ } else {
+ setIsVisible(false);
+ }
+ }, [error]);
+
+ // RequestAnimationFrame loop for smooth, pausable progress
+ useEffect(() => {
+ if (!isVisible || isPaused) {
+ prevTimeRef.current = null;
+ if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
+ return;
+ }
+
+ /**
+ * Updates the animation frame progress based on elapsed time.
+ *
+ * @param {number} now - Current timestamp provided by requestAnimationFrame.
+ */
+ const updateAnimation = (now: number) => {
+ if (prevTimeRef.current === null) {
+ prevTimeRef.current = now;
+ }
+
+ const elapsed = now - prevTimeRef.current;
+ prevTimeRef.current = now;
+
+ timeLeftRef.current = Math.max(0, timeLeftRef.current - elapsed);
+ setProgress((timeLeftRef.current / AUTO_DISMISS_TIME) * 100);
+
+ if (timeLeftRef.current <= 0) {
+ dismissToast();
+ } else {
+ animFrameRef.current = requestAnimationFrame(updateAnimation);
+ }
+ };
+
+ animFrameRef.current = requestAnimationFrame(updateAnimation);
+
+ return () => {
+ if (animFrameRef.current) cancelAnimationFrame(animFrameRef.current);
+ };
+ }, [isVisible, isPaused, dismissToast]);
+
+ if (!isVisible || !error) return null;
+
+ const message =
+ error === "unauthorized_edit"
+ ? "You cannot edit tasks that belong to others or are in the trash."
+ : error === "task_not_found"
+ ? "The requested task does not exist or has been deleted."
+ : "An unexpected error occurred.";
+
+ return (
+ setIsPaused(true)}
+ onMouseLeave={() => setIsPaused(false)}
+ className="fixed top-6 right-6 z-99 max-w-sm w-full overflow-hidden flex flex-col bg-background/80 dark:bg-zinc-950/80 backdrop-blur-xl border border-destructive/30 text-foreground rounded-2xl shadow-[0_12px_40px_-10px_hsl(0_84%_60%/0.15)] animate-in slide-in-from-top-4 fade-in duration-300 ease-out cursor-default select-none"
+ >
+
+
+
+
+
+
+ Access Denied
+
+
+
+
+ {message}
+
+
+
+
+
+
+ );
+}