refactor: Simplify imports and clean up DashboardCard component for improved readability and maintainability

This commit is contained in:
2025-09-08 16:35:22 +05:30
parent c7408f07b9
commit cac9d69539

View File

@@ -3,11 +3,7 @@ import { createPortal } from "react-dom";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import img from "../../assets/image/image.png"; import img from "../../assets/image/image.png";
import { getUserData } from "../../functions/getUserData"; import { getUserData } from "../../functions/getUserData";
import { import { useLoadingProgress, useProjectName, useSocketStore } from "../../store/builder/store";
useLoadingProgress,
useProjectName,
useSocketStore,
} from "../../store/builder/store";
import OuterClick from "../../utils/outerClick"; import OuterClick from "../../utils/outerClick";
import { KebabIcon } from "../icons/ExportCommonIcons"; import { KebabIcon } from "../icons/ExportCommonIcons";
import { getAllProjects } from "../../services/dashboard/getAllProjects"; import { getAllProjects } from "../../services/dashboard/getAllProjects";
@@ -15,323 +11,275 @@ import { getAllProjects } from "../../services/dashboard/getAllProjects";
// import { updateProject } from "../../services/dashboard/updateProject"; // import { updateProject } from "../../services/dashboard/updateProject";
interface DashBoardCardProps { interface DashBoardCardProps {
projectName: string; projectName: string;
thumbnail: string; thumbnail: string;
projectId: string; projectId: string;
createdAt?: string; createdAt?: string;
isViewed?: string; isViewed?: string;
createdBy?: { _id: string; userName: string }; createdBy?: { _id: string; userName: string };
handleDeleteProject?: (projectId: string) => Promise<void>; handleDeleteProject?: (projectId: string) => Promise<void>;
handleTrashDeleteProject?: (projectId: string) => Promise<void>; handleTrashDeleteProject?: (projectId: string) => Promise<void>;
handleRestoreProject?: (projectId: string) => Promise<void>; handleRestoreProject?: (projectId: string) => Promise<void>;
handleDuplicateWorkspaceProject?: ( handleDuplicateWorkspaceProject?: (projectId: string, projectName: string, thumbnail: string, userId?: string) => Promise<void>;
projectId: string, handleDuplicateRecentProject?: (projectId: string, projectName: string, thumbnail: string) => Promise<void>;
projectName: string, active?: "shared" | "trash" | "recent" | string;
thumbnail: string, setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>;
userId?: string setRecentDuplicateData?: React.Dispatch<React.SetStateAction<object>>;
) => Promise<void>; setProjectDuplicateData?: React.Dispatch<React.SetStateAction<object>>;
handleDuplicateRecentProject?: ( setActiveFolder?: React.Dispatch<React.SetStateAction<string>>;
projectId: string, openKebabProjectId: string | null;
projectName: string, setOpenKebabProjectId: React.Dispatch<React.SetStateAction<string | null>>;
thumbnail: string
) => Promise<void>;
active?: "shared" | "trash" | "recent" | string;
setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>;
setRecentDuplicateData?: React.Dispatch<React.SetStateAction<object>>;
setProjectDuplicateData?: React.Dispatch<React.SetStateAction<object>>;
setActiveFolder?: React.Dispatch<React.SetStateAction<string>>;
openKebabProjectId: string | null;
setOpenKebabProjectId: React.Dispatch<React.SetStateAction<string | null>>;
} }
type RelativeTimeFormatUnit = type RelativeTimeFormatUnit = "year" | "month" | "week" | "day" | "hour" | "minute" | "second";
| "year"
| "month"
| "week"
| "day"
| "hour"
| "minute"
| "second";
const kebabOptionsMap: Record<string, string[]> = { const kebabOptionsMap: Record<string, string[]> = {
default: ["rename", "delete", "duplicate", "open in new tab"], default: ["rename", "delete", "duplicate", "open in new tab"],
trash: ["restore", "delete"], trash: ["restore", "delete"],
shared: ["duplicate", "open in new tab"], shared: ["duplicate", "open in new tab"],
}; };
const DashboardCard: React.FC<DashBoardCardProps> = ({ const DashboardCard: React.FC<DashBoardCardProps> = ({
projectName,
thumbnail,
projectId,
active,
handleDeleteProject,
handleRestoreProject,
handleTrashDeleteProject,
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
createdAt,
createdBy,
setRecentDuplicateData,
setProjectDuplicateData,
setActiveFolder,
openKebabProjectId,
setOpenKebabProjectId,
}) => {
const navigate = useNavigate();
const { setProjectName } = useProjectName();
const { userId, organization, userName } = getUserData();
const { projectSocket } = useSocketStore();
const { setLoadingProgress } = useLoadingProgress();
const isKebabOpen = openKebabProjectId === projectId;
const [renameValue, setRenameValue] = useState(projectName);
const [isRenaming, setIsRenaming] = useState(false);
const kebabRef = useRef<HTMLDivElement>(null);
// Close kebab when clicking outside
OuterClick({
contextClassName: [`tag-${projectId}`],
setMenuVisible: () => {
if (isKebabOpen) setOpenKebabProjectId(null);
},
});
const navigateToProject = useCallback(() => {
if (active === "trash") return;
setLoadingProgress(1);
setProjectName(projectName);
navigate(`/projects/${projectId}`);
}, [
active,
projectId,
projectName, projectName,
navigate, thumbnail,
setLoadingProgress, projectId,
setProjectName, active,
]); handleDeleteProject,
handleRestoreProject,
handleTrashDeleteProject,
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
createdAt,
createdBy,
setRecentDuplicateData,
setProjectDuplicateData,
setActiveFolder,
openKebabProjectId,
setOpenKebabProjectId,
}) => {
const navigate = useNavigate();
const { setProjectName } = useProjectName();
const { userId, organization, userName } = getUserData();
const { projectSocket } = useSocketStore();
const { setLoadingProgress } = useLoadingProgress();
const getOptions = useCallback(() => { const isKebabOpen = openKebabProjectId === projectId;
if (active === "trash") return kebabOptionsMap.trash; const [renameValue, setRenameValue] = useState(projectName);
if (active === "shared" || (createdBy && createdBy._id !== userId)) { const [isRenaming, setIsRenaming] = useState(false);
return kebabOptionsMap.shared; const kebabRef = useRef<HTMLDivElement>(null);
}
return kebabOptionsMap.default;
}, [active, createdBy, userId]);
const handleProjectName = useCallback( // Close kebab when clicking outside
async (newName: string) => { OuterClick({
setRenameValue(newName); contextClassName: [`tag-${projectId}`],
if (!projectId) return; setMenuVisible: () => {
if (isKebabOpen) setOpenKebabProjectId(null);
},
});
try { const navigateToProject = useCallback(() => {
const projects = await getAllProjects(userId, organization); if (active === "trash") return;
const projectUuid = projects?.Projects?.find( setLoadingProgress(1);
(val: any) => val.projectUuid === projectId || val._id === projectId setProjectName(projectName);
); navigate(`/projects/${projectId}`);
if (!projectUuid) return; }, [active, projectId, projectName, navigate, setLoadingProgress, setProjectName]);
const updatePayload = { const getOptions = useCallback(() => {
projectId: projectUuid._id, if (active === "trash") return kebabOptionsMap.trash;
organization, if (active === "shared" || (createdBy && createdBy._id !== userId)) {
userId, return kebabOptionsMap.shared;
projectName: newName, }
return kebabOptionsMap.default;
}, [active, createdBy, userId]);
const handleProjectName = useCallback(
async (newName: string) => {
setRenameValue(newName);
if (!projectId) return;
try {
const projects = await getAllProjects(userId, organization);
const projectUuid = projects?.Projects?.find((val: any) => val.projectUuid === projectId || val._id === projectId);
if (!projectUuid) return;
const updatePayload = {
projectId: projectUuid._id,
organization,
userId,
projectName: newName,
};
if (projectSocket) {
projectSocket.emit("v1:project:update", updatePayload);
}
} catch {
// silent fail
}
},
[projectId, userId, organization, projectSocket]
);
const handleOptionClick = useCallback(
async (option: string) => {
switch (option) {
case "delete":
await (active === "trash" ? handleTrashDeleteProject?.(projectId) : handleDeleteProject?.(projectId));
break;
case "restore":
await handleRestoreProject?.(projectId);
break;
case "open in new tab":
setProjectName(projectName);
window.open(`/projects/${projectId}`, "_blank");
break;
case "rename":
setIsRenaming(true);
break;
case "duplicate":
if (handleDuplicateWorkspaceProject) {
setProjectDuplicateData?.({ projectId, projectName, thumbnail });
await handleDuplicateWorkspaceProject(projectId, projectName, thumbnail, userId);
if (active === "shared") {
setActiveFolder?.("myProjects");
}
} else if (handleDuplicateRecentProject) {
setRecentDuplicateData?.({
projectId,
projectName,
thumbnail,
userId,
});
await handleDuplicateRecentProject(projectId, projectName, thumbnail);
}
break;
}
},
[
projectId,
projectName,
thumbnail,
userId,
active,
handleDeleteProject,
handleTrashDeleteProject,
handleRestoreProject,
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
setProjectName,
setProjectDuplicateData,
setRecentDuplicateData,
setActiveFolder,
]
);
const getRelativeTime = useCallback((dateString: string): string => {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const intervals: Record<RelativeTimeFormatUnit, number> = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
}; };
if (projectSocket) { const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
projectSocket.emit("v1:project:update", updatePayload); for (const [unit, seconds] of Object.entries(intervals)) {
const diff = Math.floor(diffInSeconds / seconds);
if (diff >= 1) return rtf.format(-diff, unit as RelativeTimeFormatUnit);
} }
} catch { return "just now";
// silent fail }, []);
}
},
[projectId, userId, organization, projectSocket]
);
const handleOptionClick = useCallback( const [kebabPosition, setKebabPosition] = useState({ top: 0, left: 0 });
async (option: string) => {
switch (option) { useEffect(() => {
case "delete": if (isKebabOpen && kebabRef.current) {
await (handleDeleteProject?.(projectId) ?? const rect = kebabRef.current.getBoundingClientRect();
handleTrashDeleteProject?.(projectId)); setKebabPosition({
break; top: rect.bottom + window.scrollY,
case "restore": left: rect.left + window.scrollX - 80,
await handleRestoreProject?.(projectId);
break;
case "open in new tab":
setProjectName(projectName);
window.open(`/projects/${projectId}`, "_blank");
break;
case "rename":
setIsRenaming(true);
break;
case "duplicate":
if (handleDuplicateWorkspaceProject) {
setProjectDuplicateData?.({ projectId, projectName, thumbnail });
await handleDuplicateWorkspaceProject(
projectId,
projectName,
thumbnail,
userId
);
if (active === "shared") {
setActiveFolder?.("myProjects");
}
} else if (handleDuplicateRecentProject) {
setRecentDuplicateData?.({
projectId,
projectName,
thumbnail,
userId,
}); });
await handleDuplicateRecentProject( }
projectId, }, [isKebabOpen]);
projectName,
thumbnail
);
}
break;
}
},
[
projectId,
projectName,
thumbnail,
userId,
active,
handleDeleteProject,
handleTrashDeleteProject,
handleRestoreProject,
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
setProjectName,
setProjectDuplicateData,
setRecentDuplicateData,
setActiveFolder,
]
);
const getRelativeTime = useCallback((dateString: string): string => { return (
const date = new Date(dateString); <div className="dashboard-card-container" onClick={navigateToProject} title={projectName}>
const now = new Date(); <div className="dashboard-card-wrapper">
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); <div className="preview-container">
<img src={thumbnail || img} alt={`${projectName} thumbnail`} />
</div>
const intervals: Record<RelativeTimeFormatUnit, number> = { <div className="project-details-container" onClick={(e) => e.stopPropagation()}>
year: 31536000, <div className="project-details">
month: 2592000, {isRenaming ? (
week: 604800, <input
day: 86400, value={renameValue}
hour: 3600, onChange={(e) => handleProjectName(e.target.value)}
minute: 60, onBlur={() => {
second: 1, setIsRenaming(false);
}; setProjectName(renameValue);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setIsRenaming(false);
setProjectName(renameValue);
}
}}
aria-label="Rename project"
autoFocus
/>
) : (
<span>{renameValue}</span>
)}
{createdAt && (
<div className="project-data">
{active === "trash" ? "Trashed" : "Edited"} {getRelativeTime(createdAt)}
</div>
)}
</div>
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" }); <div className="users-list-container" ref={kebabRef}>
for (const [unit, seconds] of Object.entries(intervals)) { <div className="user-profile">{(createdBy?.userName || userName || "A").charAt(0).toUpperCase()}</div>
const diff = Math.floor(diffInSeconds / seconds); <button
if (diff >= 1) return rtf.format(-diff, unit as RelativeTimeFormatUnit); className="kebab-wrapper"
} onClick={(e) => {
return "just now"; e.stopPropagation();
}, []); setOpenKebabProjectId(isKebabOpen ? null : projectId);
}}
const [kebabPosition, setKebabPosition] = useState({ top: 0, left: 0 }); aria-haspopup="true"
aria-expanded={isKebabOpen}
useEffect(() => { aria-label="Project options"
if (isKebabOpen && kebabRef.current) { >
const rect = kebabRef.current.getBoundingClientRect(); <KebabIcon />
setKebabPosition({ </button>
top: rect.bottom + window.scrollY, </div>
left: rect.left + window.scrollX - 80, </div>
});
}
}, [isKebabOpen]);
return (
<div
className="dashboard-card-container"
onClick={navigateToProject}
title={projectName}
>
<div className="dashboard-card-wrapper">
<div className="preview-container">
<img src={thumbnail || img} alt={`${projectName} thumbnail`} />
</div>
<div
className="project-details-container"
onClick={(e) => e.stopPropagation()}
>
<div className="project-details">
{isRenaming ? (
<input
value={renameValue}
onChange={(e) => handleProjectName(e.target.value)}
onBlur={() => {
setIsRenaming(false);
setProjectName(renameValue);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setIsRenaming(false);
setProjectName(renameValue);
}
}}
aria-label="Rename project"
autoFocus
/>
) : (
<span>{renameValue}</span>
)}
{createdAt && (
<div className="project-data">
{active === "trash" ? "Trashed" : "Edited"}{" "}
{getRelativeTime(createdAt)}
</div>
)}
</div>
<div className="users-list-container" ref={kebabRef}>
<div className="user-profile">
{(createdBy?.userName || userName || "A").charAt(0).toUpperCase()}
</div> </div>
<button
className="kebab-wrapper"
onClick={(e) => {
e.stopPropagation();
setOpenKebabProjectId(isKebabOpen ? null : projectId);
}}
aria-haspopup="true"
aria-expanded={isKebabOpen}
aria-label="Project options"
>
<KebabIcon />
</button>
</div>
</div>
</div>
{isKebabOpen && {isKebabOpen &&
createPortal( createPortal(
<div <div className={`kebab-options-wrapper tag-${projectId}`} style={{ position: "fixed", zIndex: 9999, ...kebabPosition }}>
className={`kebab-options-wrapper tag-${projectId}`} {getOptions().map((option) => (
style={{ position: "fixed", zIndex: 9999, ...kebabPosition }} <button
> key={option}
{getOptions().map((option) => ( className="option"
<button onClick={(e) => {
key={option} e.stopPropagation();
className="option" handleOptionClick(option);
onClick={(e) => { }}
e.stopPropagation(); >
handleOptionClick(option); {option}
}} </button>
> ))}
{option} </div>,
</button> document.body
))} )}
</div>, </div>
document.body );
)}
</div>
);
}; };
export default DashboardCard; export default DashboardCard;