From 8bf1aa41e3c68a15be4938fd433b6af782b0ab40 Mon Sep 17 00:00:00 2001 From: Chneemann Date: Wed, 19 Aug 2026 04:05:30 +0200 Subject: [PATCH] fix(member): add error handling and toast feedback for missing user profiles --- app/(app)/member/[id]/page.tsx | 36 ++++++++++---------- app/components/ui/ErrorToast.tsx | 57 ++++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/app/(app)/member/[id]/page.tsx b/app/(app)/member/[id]/page.tsx index 8746a58..2a3c41f 100644 --- a/app/(app)/member/[id]/page.tsx +++ b/app/(app)/member/[id]/page.tsx @@ -1,38 +1,40 @@ /** * @file member/[id]/page.tsx - * @description Server component rendering the detailed profile page for a specific member by fetching their user profile data. + * @description Server component rendering the detailed profile page for a specific member by validating the ID format and fetching user profile data. */ import { UserService } from "@/services/user.service"; -import { notFound } from "next/navigation"; +import { redirect } from "next/navigation"; import MemberHeader from "../MemberHeader"; import MemberProfileCard from "./MemberProfileCard"; /** - * Properties for the MemberDetailPage component. - * - * @interface MemberDetailPageProps - * @property {Promise<{ id: string }>} params - Promise containing route context parameters with the target member ID. - */ -interface MemberDetailPageProps { - params: Promise<{ id: string }>; -} - -/** - * Renders the member detail page by resolving the route parameter, fetching the user profile by ID, - * and displaying their header and profile card or triggering a 404 page if the member is not found. + * Renders the member detail page by resolving route parameters, validating the member ID syntax, + * fetching profile data, and displaying the header and profile card or redirecting with an error parameter if not found. * * @async - * @param {MemberDetailPageProps} props - The component props. + * @param {Object} props - The component props. + * @param {Promise<{ id: string }>} props.params - Promise containing route context parameters with the target member ID. * @returns {Promise} The rendered member detail page component. */ export default async function MemberDetailPage({ params, -}: MemberDetailPageProps) { +}: { + params: Promise<{ id: string }>; +}) { const { id } = await params; + const UUID_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + + if (!UUID_REGEX.test(id)) { + redirect("/member?error=member_not_found"); + } + const user = await UserService.findProfileById(id); - if (!user) notFound(); + if (!user) { + redirect("/member?error=member_not_found"); + } return (
diff --git a/app/components/ui/ErrorToast.tsx b/app/components/ui/ErrorToast.tsx index 270e114..86ef71a 100644 --- a/app/components/ui/ErrorToast.tsx +++ b/app/components/ui/ErrorToast.tsx @@ -5,7 +5,7 @@ "use client"; -import { useSearchParams, useRouter } from "next/navigation"; +import { useSearchParams, useRouter, usePathname } from "next/navigation"; import { useEffect, useState, useRef, useCallback } from "react"; import { AlertCircle, X } from "lucide-react"; @@ -19,6 +19,7 @@ const AUTO_DISMISS_TIME = 5000; */ export default function ErrorToast() { const searchParams = useSearchParams(); + const pathname = usePathname(); const router = useRouter(); const error = searchParams.get("error"); @@ -31,12 +32,19 @@ export default function ErrorToast() { const animFrameRef = useRef(null); /** - * Dismisses the toast notification and cleans up URL parameters. + * Dismisses the toast notification and cleans up the URL error parameter while preserving the current pathname. */ const dismissToast = useCallback(() => { setIsVisible(false); - router.replace("/dashboard", { scroll: false }); - }, [router]); + + // Clean up query parameters without forcing a hard redirect to dashboard + const params = new URLSearchParams(searchParams.toString()); + params.delete("error"); + const query = params.toString(); + const newPath = query ? `${pathname}?${query}` : pathname; + + router.replace(newPath, { scroll: false }); + }, [router, pathname, searchParams]); // Show a toast message when an error occurs and reset the timer useEffect(() => { @@ -90,12 +98,39 @@ export default function ErrorToast() { 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."; + /** + * Resolves the user-facing error title and detailed message based on the error query parameter. + * + * @returns {{ title: string; message: string }} An object containing the toast header title and body message. + */ + const getErrorInfo = (): { title: string; message: string } => { + switch (error) { + case "unauthorized_edit": + return { + title: "Access Denied", + message: + "You cannot edit tasks that belong to others or are in the trash.", + }; + case "task_not_found": + return { + title: "Task Not Found", + message: "The requested task does not exist or has been deleted.", + }; + case "member_not_found": + return { + title: "Member Not Found", + message: + "The requested team member does not exist or has been removed.", + }; + default: + return { + title: "Error", + message: "An unexpected error occurred.", + }; + } + }; + + const { title, message } = getErrorInfo(); return (

- Access Denied + {title}