diff --git a/app/src/components/Dashboard/DashboardCard.tsx b/app/src/components/Dashboard/DashboardCard.tsx index 36208d9..75fcdf8 100644 --- a/app/src/components/Dashboard/DashboardCard.tsx +++ b/app/src/components/Dashboard/DashboardCard.tsx @@ -270,6 +270,7 @@ const DashboardCard: React.FC = ({ onClick={(e) => { e.stopPropagation(); handleOptionClick(option); + setOpenKebabProjectId(null); }} > {option} diff --git a/app/src/components/Dashboard/DashboardMain.tsx b/app/src/components/Dashboard/DashboardMain.tsx index a922263..56717e5 100644 --- a/app/src/components/Dashboard/DashboardMain.tsx +++ b/app/src/components/Dashboard/DashboardMain.tsx @@ -13,22 +13,7 @@ import { trashSearchProjectApi } from "../../services/dashboard/trashSearchProje import { restoreTrashApi } from "../../services/dashboard/restoreTrashApi"; import { generateUniqueId } from "../../functions/generateUniqueId"; import ProjectSocketRes from "./socket/projectSocketRes"; - -interface Project { - _id: string; - projectName: string; - thumbnail: string; - createdBy: { _id: string; userName: string }; - projectUuid?: string; - createdAt?: string; - DeletedAt?: string; -} - -interface ProjectCollection { - [key: string]: Project[]; -} - -type Folder = "home" | "projects" | "shared" | "trash"; +import { Modal } from "../templates/PreviewModal"; interface DashboardMainProps { activeFolder: Folder; @@ -36,11 +21,16 @@ interface DashboardMainProps { const DashboardMain: React.FC = ({ activeFolder }) => { const [activeSubFolder, setActiveSubFolder] = useState("myProjects"); - const [projectsData, setProjectsData] = useState({}); + const [projectsData, setProjectsData] = useState({}); const [isSearchActive, setIsSearchActive] = useState(false); const [duplicateData, setDuplicateData] = useState({}); const [openKebabProjectId, setOpenKebabProjectId] = useState(null); - const [projectsCache, setProjectsCache] = useState<{ [key: string]: ProjectCollection }>({}); + const [projectsCache, setProjectsCache] = useState<{ + [key: string]: DashboardProjectCollection; + }>({}); + const [showDelete, setShowDelete] = useState(false); + const [confirmText, setConfirmText] = useState(""); // 🔹 For confirmation input + const [deleteTargetId, setDeleteTargetId] = useState(null); // 🔹 Store project id to delete const { userId, organization } = getUserData(); const { projectSocket } = useSocketStore(); @@ -55,7 +45,7 @@ const DashboardMain: React.FC = ({ activeFolder }) => { } try { - let projects: ProjectCollection = {}; // initialize as empty object + let projects: DashboardProjectCollection = {}; switch (activeFolder) { case "home": @@ -71,9 +61,10 @@ const DashboardMain: React.FC = ({ activeFolder }) => { case "trash": projects = await getTrashApi(); break; + default: + return; } - // Only update cache if projects is not empty if (projects && JSON.stringify(projects) !== JSON.stringify(projectsData)) { setProjectsCache((prev) => ({ ...prev, [cacheKey]: projects })); setProjectsData(projects); @@ -113,15 +104,20 @@ const DashboardMain: React.FC = ({ activeFolder }) => { updateStateAfterRemove(projectId); }; - const handlePermanentDelete = async (projectId: string): Promise => { + const handlePermanentDelete = async (): Promise => { + if (!deleteTargetId) return; + if (projectSocket) { projectSocket.emit("v1:trash:delete", { - projectId, + projectId: deleteTargetId, organization, userId, }); } - updateStateAfterRemove(projectId); + updateStateAfterRemove(deleteTargetId); + setShowDelete(false); + setConfirmText(""); + setDeleteTargetId(null); }; const handleRestore = async (projectId: string): Promise => { @@ -129,7 +125,11 @@ const DashboardMain: React.FC = ({ activeFolder }) => { updateStateAfterRemove(projectId); }; - const handleDuplicate = async (projectId: string, projectName: string, thumbnail: string): Promise => { + const handleDuplicate = async ( + projectId: string, + projectName: string, + thumbnail: string + ): Promise => { if (projectSocket) { projectSocket.emit("v1:project:Duplicate", { userId, @@ -144,7 +144,7 @@ const DashboardMain: React.FC = ({ activeFolder }) => { // #region Project Map const updateStateAfterRemove = (projectId: string) => { - setProjectsData((prev: ProjectCollection) => { + setProjectsData((prev: DashboardProjectCollection) => { const key = Object.keys(prev)[0]; const updatedList = prev[key]?.filter((p) => p._id !== projectId) || []; return { ...prev, [key]: updatedList }; @@ -185,7 +185,10 @@ const DashboardMain: React.FC = ({ activeFolder }) => { })} {...(activeFolder === "trash" && { handleRestoreProject: handleRestore, - handleTrashDeleteProject: handlePermanentDelete, + handleTrashDeleteProject: async (id: string): Promise => { + setDeleteTargetId(id); + setShowDelete(true); + }, active: "trash", })} openKebabProjectId={openKebabProjectId} @@ -204,7 +207,12 @@ const DashboardMain: React.FC = ({ activeFolder }) => {
{activeFolder === "home" && } @@ -212,10 +220,16 @@ const DashboardMain: React.FC = ({ activeFolder }) => {
{activeFolder === "projects" && (
- -
@@ -223,8 +237,40 @@ const DashboardMain: React.FC = ({ activeFolder }) => {
{renderProjects()}
- +
+ + {/* 🔹 Delete Confirmation Modal */} + setShowDelete(false)} + type="confirm" + title="Delete Project" + description="This action is permanent. Please type DELETE to confirm." + inputs={[ + { + id: "confirmDelete", + label: "Confirmation", + placeholder: "Type DELETE", + value: confirmText, + onChange: setConfirmText, + }, + ]} + buttons={[ + { label: "Cancel", onClick: () => setShowDelete(false), variant: "secondary" }, + { + label: "Delete", + onClick: handlePermanentDelete, + variant: "danger", + disabled: confirmText !== "DELETE", + }, + ]} + />
); }; diff --git a/app/src/components/Dashboard/DashboardTutorial.tsx b/app/src/components/Dashboard/DashboardTutorial.tsx index f8c1d25..ce90bc7 100644 --- a/app/src/components/Dashboard/DashboardTutorial.tsx +++ b/app/src/components/Dashboard/DashboardTutorial.tsx @@ -1,61 +1,78 @@ -import { useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import DashboardNavBar from "./DashboardNavBar"; -import DashboardCard from "./DashboardCard"; import { projectTutorialApi } from "../../services/dashboard/projectTutorialApi"; +import { AIIcon } from "../icons/ExportCommonIcons"; +import { DeleteIcon } from "../icons/ContextMenuIcons"; -interface Project { +interface Tutorial { _id: string; - projectName: string; - thumbnail: string; - createdBy: string; - projectUuid?: string; + name: string; + thumbnail?: string; + updatedAt: string; } -interface DiscardedProjects { - [key: string]: Project[]; -} +const TutorialCard: React.FC<{ tutorial: Tutorial }> = ({ tutorial }) => { + return ( +
+
+
+
{tutorial.name}
+
+ {new Date(tutorial.updatedAt).toLocaleDateString()} +
+
+ +
+
+
+ ); +}; -const DashboardTutorial = () => { - const [tutorialProject, setTutorialProject] = useState({}); - - const handleIcon = async () => { - try { - let tutorial = await projectTutorialApi(); - setTutorialProject(tutorial); - } catch {} - }; - - const [openKebabProjectId, setOpenKebabProjectId] = useState(null); +const DashboardTutorial: React.FC = () => { + const [tutorials, setTutorials] = useState([]); useEffect(() => { - handleIcon(); + const fetchTutorials = async () => { + try { + const res = await projectTutorialApi(); + if (res && Array.isArray(res)) { + setTutorials(res); + } + } catch (error) { + console.error("Error fetching tutorials:", error); + } + }; + + fetchTutorials(); }, []); - const renderTrashProjects = () => { - const projectList = tutorialProject[Object.keys(tutorialProject)[0]]; - - if (!projectList?.length) { - return
No deleted projects found
; - } - - return projectList.map((tutorials: any) => ( - - )); - }; return (
-
-
-
{renderTrashProjects()}
+
+
+ + +
+
+ +
+ {tutorials.length > 0 ? ( + tutorials.map((tut) => ) + ) : ( +
No tutorials available
+ )} +
); diff --git a/app/src/components/Dashboard/SidePannel.tsx b/app/src/components/Dashboard/SidePannel.tsx index 50dda20..af495ab 100644 --- a/app/src/components/Dashboard/SidePannel.tsx +++ b/app/src/components/Dashboard/SidePannel.tsx @@ -1,9 +1,18 @@ import React from "react"; -import { DocumentationIcon, HelpIcon, HomeIcon, LogoutIcon, NotificationIcon, ProjectsIcon, TutorialsIcon } from "../icons/DashboardIcon"; +import { + DocumentationIcon, + HelpIcon, + HomeIcon, + LogoutIcon, + NotificationIcon, + ProjectsIcon, + TrashIcon, + TutorialsIcon, +} from "../icons/DashboardIcon"; import { useNavigate } from "react-router-dom"; import darkThemeImage from "../../assets/image/darkThemeProject.png"; import lightThemeImage from "../../assets/image/lightThemeProject.png"; -import { SettingsIcon, TrashIcon } from "../icons/ExportCommonIcons"; +import { SettingsIcon } from "../icons/ExportCommonIcons"; import { getUserData } from "../../functions/getUserData"; import { useSocketStore } from "../../store/builder/store"; @@ -74,16 +83,33 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => {
- - -
diff --git a/app/src/components/Dashboard/socket/projectSocketRes.tsx b/app/src/components/Dashboard/socket/projectSocketRes.tsx index 66065a2..e6f837f 100644 --- a/app/src/components/Dashboard/socket/projectSocketRes.tsx +++ b/app/src/components/Dashboard/socket/projectSocketRes.tsx @@ -5,24 +5,14 @@ import { getAllProjectsApi } from "../../../services/dashboard/getAllProjectsApi import { recentlyViewedApi } from "../../../services/dashboard/recentlyViewedApi"; import { useNavigate } from "react-router-dom"; -interface Project { - _id: string; - projectName: string; - thumbnail: string; - createdBy: { _id: string; userName: string }; - projectUuid?: string; - createdAt?: string; - DeletedAt?: string; - isViewed?: string; -} -interface ProjectCollection { - [key: string]: Project[]; +interface DashboardProjectCollection { + [key: string]: DashboardProject[]; } interface ProjectSocketResProps { - setRecentProjects?: React.Dispatch>; - setWorkspaceProjects?: React.Dispatch>; + setRecentProjects?: React.Dispatch>; + setWorkspaceProjects?: React.Dispatch>; setIsSearchActive?: React.Dispatch>; } diff --git a/app/src/components/icons/DashboardIcon.tsx b/app/src/components/icons/DashboardIcon.tsx index dbcfb7b..ac2f166 100644 --- a/app/src/components/icons/DashboardIcon.tsx +++ b/app/src/components/icons/DashboardIcon.tsx @@ -1,272 +1,297 @@ export function NotificationIcon() { - return ( - - - - - ); + return ( + + + + + ); } -export function HomeIcon() { - return ( - - - - ); +export function HomeIcon({ isActive }: Readonly<{ isActive: boolean }>) { + return ( + + + + ); } -export function ProjectsIcon() { - return ( - - - - ); +export function ProjectsIcon({ isActive }: Readonly<{ isActive: boolean }>) { + return ( + + + + ); } -export function TutorialsIcon() { - return ( - - - - - - - - - - - - - - - - - - - - ); +export function TrashIcon({ isActive }: Readonly<{ isActive: boolean }>) { + return ( + + + + + + + + + + + ); } -export function DocumentationIcon() { - return ( - - - - - ); +export function TutorialsIcon({ isActive }: Readonly<{ isActive: boolean }>) { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export function DocumentationIcon({ isActive }: Readonly<{ isActive: boolean }>) { + return ( + + + + + ); } export function HelpIcon() { - return ( - - - - - - - - - - - ); + height="13" + viewBox="0 0 12 13" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + + + + + + + + + + ); } export function LogoutIcon() { - return ( - - - - - - ); + return ( + + + + + + ); } export function WifiIcon() { - return ( - - - - ); + return ( + + + + ); } diff --git a/app/src/components/icons/SimulationIcons.tsx b/app/src/components/icons/SimulationIcons.tsx index 95844ad..477b5f3 100644 --- a/app/src/components/icons/SimulationIcons.tsx +++ b/app/src/components/icons/SimulationIcons.tsx @@ -1,863 +1,830 @@ export function AnalysisIcon({ isActive }: Readonly<{ isActive: boolean }>) { - return ( - - - - - ); + return ( + + + + + ); } export function MechanicsIcon({ isActive }: Readonly<{ isActive: boolean }>) { - return ( - - - - - ); + return ( + + + + + ); } export function PropertiesIcon({ isActive }: Readonly<{ isActive: boolean }>) { - return ( - - - - - - ); + return ( + + + + + + ); } export function SimulationIcon({ isActive }: Readonly<{ isActive: boolean }>) { - return ( - - - - - ); + return ( + + + + + ); } // simulation player icons export function ResetIcon() { - return ( - - - - - - ); + return ( + + + + + + ); } export function PlayStopIcon() { - return ( - - - - ); + return ( + + + + ); } export function ExitIcon() { - return ( - - - - ); + return ( + + + + ); } export function MoveArrowRight() { - return ( - - - - ); + return ( + + + + ); } export function MoveArrowLeft() { - return ( - - - - ); + return ( + + + + ); } // simulation card icons -export function ExpandIcon({ - color = "#6F42C1", -}: Readonly<{ color?: string }>) { - return ( - - - - - ); +export function ExpandIcon({ color = "#6F42C1" }: Readonly<{ color?: string }>) { + return ( + + + + + ); } -export function SimulationStatusIcon({ - color = "#21FF59", -}: Readonly<{ color?: string }>) { - return ( - - - - - ); +export function SimulationStatusIcon({ color = "#21FF59" }: Readonly<{ color?: string }>) { + return ( + + + + + ); } export function IndicationArrow() { - return ( - - - - ); + return ( + + + + ); } export function CartBagIcon() { - return ( - - - - - - - - ); + return ( + + + + + + + + ); } export function StorageCapacityIcon() { - return ( - - - - - - - - - - - - - - - - - - - ); + viewBox="0 0 21 20" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + + + + + + + + + + + + + + + + + + ); } export function CompareLayoutIcon() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } export const ResizerIcon = () => { - return ( - - - - - - - - - - - - - - - - - - - ); + + + + + + + + + + + + + + + + + + ); }; export const LayoutIcon = () => { - return ( - - - - - - ); + return ( + + + + + + ); }; - export function FilePackageIcon({ isActive }: Readonly<{ isActive: boolean }>) { - - return ( - - - - - - - - - - - - - - - - - - - - ) -} \ No newline at end of file + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/src/components/templates/PreviewModal.tsx b/app/src/components/templates/PreviewModal.tsx new file mode 100644 index 0000000..c0797da --- /dev/null +++ b/app/src/components/templates/PreviewModal.tsx @@ -0,0 +1,132 @@ +import React, { ReactNode } from "react"; +import clsx from "clsx"; +import { CloseIcon } from "../icons/ExportCommonIcons"; +import RenderOverlay from "./Overlay"; + +export type ModalType = "info" | "warning" | "error" | "success" | "confirm" | "action"; + +export interface ModalButton { + label: string; + onClick: () => void; + variant?: "primary" | "secondary" | "danger" | "ghost"; + disabled?: boolean; +} + +export interface ModalInput { + id: string; + label: string; + type?: "text" | "number" | "password" | "textarea"; + placeholder?: string; + value: string; + onChange: (value: string) => void; +} + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + type?: ModalType; + title?: string; + description?: string | ReactNode; + note?: string | ReactNode; + inputs?: ModalInput[]; + buttons?: ModalButton[]; + children?: ReactNode; // optional custom content + closeOnOverlayClick?: boolean; +} + +const typeColors: Record = { + info: "modal-title--info", + warning: "modal-title--warning", + error: "modal-title--error", + success: "modal-title--success", + confirm: "modal-title--confirm", + action: "modal-title--action", +}; + +export const Modal: React.FC = ({ + isOpen, + onClose, + type = "info", + title, + description, + note, + inputs, + buttons, + children, + closeOnOverlayClick = true, +}) => { + if (!isOpen) return null; + + return ( + +
+
e.stopPropagation()}> + {/* Header */} + + + {title &&

{title}

} + + {description &&
{description}
} + + {/* Inputs */} + {inputs && inputs.length > 0 && ( +
+ {inputs.map((input) => ( +
+ + {input.type === "textarea" ? ( +