fix(member): add error handling and toast feedback for missing user profiles
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 46s

This commit is contained in:
Chneemann 2026-08-19 04:05:30 +02:00
parent a457ad19da
commit 8bf1aa41e3
No known key found for this signature in database
2 changed files with 65 additions and 28 deletions

View file

@ -1,38 +1,40 @@
/** /**
* @file member/[id]/page.tsx * @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 { UserService } from "@/services/user.service";
import { notFound } from "next/navigation"; import { redirect } from "next/navigation";
import MemberHeader from "../MemberHeader"; import MemberHeader from "../MemberHeader";
import MemberProfileCard from "./MemberProfileCard"; import MemberProfileCard from "./MemberProfileCard";
/** /**
* Properties for the MemberDetailPage component. * 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.
* @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.
* *
* @async * @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<JSX.Element>} The rendered member detail page component. * @returns {Promise<JSX.Element>} The rendered member detail page component.
*/ */
export default async function MemberDetailPage({ export default async function MemberDetailPage({
params, params,
}: MemberDetailPageProps) { }: {
params: Promise<{ id: string }>;
}) {
const { id } = await params; 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); const user = await UserService.findProfileById(id);
if (!user) notFound(); if (!user) {
redirect("/member?error=member_not_found");
}
return ( return (
<div className="space-y-6 max-w-4xl mx-auto pb-12"> <div className="space-y-6 max-w-4xl mx-auto pb-12">

View file

@ -5,7 +5,7 @@
"use client"; "use client";
import { useSearchParams, useRouter } from "next/navigation"; import { useSearchParams, useRouter, usePathname } from "next/navigation";
import { useEffect, useState, useRef, useCallback } from "react"; import { useEffect, useState, useRef, useCallback } from "react";
import { AlertCircle, X } from "lucide-react"; import { AlertCircle, X } from "lucide-react";
@ -19,6 +19,7 @@ const AUTO_DISMISS_TIME = 5000;
*/ */
export default function ErrorToast() { export default function ErrorToast() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter(); const router = useRouter();
const error = searchParams.get("error"); const error = searchParams.get("error");
@ -31,12 +32,19 @@ export default function ErrorToast() {
const animFrameRef = useRef<number | null>(null); const animFrameRef = useRef<number | null>(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(() => { const dismissToast = useCallback(() => {
setIsVisible(false); 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 // Show a toast message when an error occurs and reset the timer
useEffect(() => { useEffect(() => {
@ -90,12 +98,39 @@ export default function ErrorToast() {
if (!isVisible || !error) return null; if (!isVisible || !error) return null;
const message = /**
error === "unauthorized_edit" * Resolves the user-facing error title and detailed message based on the error query parameter.
? "You cannot edit tasks that belong to others or are in the trash." *
: error === "task_not_found" * @returns {{ title: string; message: string }} An object containing the toast header title and body message.
? "The requested task does not exist or has been deleted." */
: "An unexpected error occurred."; 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 ( return (
<div <div
@ -111,7 +146,7 @@ export default function ErrorToast() {
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="font-semibold tracking-wider uppercase text-destructive text-sm"> <h4 className="font-semibold tracking-wider uppercase text-destructive text-sm">
Access Denied {title}
</h4> </h4>
<button <button
onClick={dismissToast} onClick={dismissToast}