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
|
* @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">
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue