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
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 46s
This commit is contained in:
parent
a457ad19da
commit
8bf1aa41e3
2 changed files with 65 additions and 28 deletions
|
|
@ -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<JSX.Element>} 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 (
|
||||
<div className="space-y-6 max-w-4xl mx-auto pb-12">
|
||||
|
|
|
|||
|
|
@ -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<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(() => {
|
||||
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 (
|
||||
<div
|
||||
|
|
@ -111,7 +146,7 @@ export default function ErrorToast() {
|
|||
<div className="flex flex-col flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="font-semibold tracking-wider uppercase text-destructive text-sm">
|
||||
Access Denied
|
||||
{title}
|
||||
</h4>
|
||||
<button
|
||||
onClick={dismissToast}
|
||||
|
|
|
|||
Loading…
Reference in a new issue