Merge remote-tracking branch 'origin/main-demo' into main-dev
This commit is contained in:
76
app/src/components/Dashboard/DasboardUseCases.tsx
Normal file
76
app/src/components/Dashboard/DasboardUseCases.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import DashboardNavBar from "./DashboardNavBar";
|
||||
import { projectTutorialApi } from "../../services/dashboard/projectTutorialApi";
|
||||
import { TutorialCard } from "./TutorialCard";
|
||||
import { getUserData } from "../../functions/getUserData";
|
||||
import { ALPHA_ORG } from "../../pages/Dashboard";
|
||||
|
||||
const DUMMY_DATA = [
|
||||
//remove later
|
||||
{
|
||||
_id: "1",
|
||||
name: "Robotic Arm Control",
|
||||
thumbnail: "https://signfix.com.au/wp-content/uploads/2017/09/placeholder-600x400.png",
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
_id: "2",
|
||||
name: "Simulation Basics",
|
||||
thumbnail: "https://signfix.com.au/wp-content/uploads/2017/09/placeholder-600x400.png",
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
_id: "3",
|
||||
name: "3D Visualization",
|
||||
thumbnail: "https://signfix.com.au/wp-content/uploads/2017/09/placeholder-600x400.png",
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
];
|
||||
|
||||
const DashboardUseCases: React.FC = () => {
|
||||
const [useCases, setUseCases] = useState<Tutorial[]>(DUMMY_DATA || []); // default []
|
||||
const { organization } = getUserData();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTutorials = async () => {
|
||||
try {
|
||||
const res = await projectTutorialApi();
|
||||
if (res && Array.isArray(res) && res.length > 0) {
|
||||
setUseCases(res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching useCases:", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTutorials();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="dashboard-home-container">
|
||||
<DashboardNavBar page="tutorial" />
|
||||
<div className="dashboard-container" style={{ height: "calc(100% - 87px)" }}>
|
||||
<div className="tutorials-list">
|
||||
{organization === ALPHA_ORG && (
|
||||
<div className="tutorials-main-header">
|
||||
<div className="tutorial-buttons-container">
|
||||
<button className="add-tutorials-button">
|
||||
<span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{useCases.length > 0 ? (
|
||||
useCases.map((tut) => <TutorialCard key={tut._id} tutorial={tut} />)
|
||||
) : (
|
||||
<div className="empty-state">
|
||||
No Use Cases available click on '+' button to add
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardUseCases;
|
||||
@@ -8,6 +8,7 @@ import OuterClick from "../../utils/outerClick";
|
||||
import { KebabIcon } from "../icons/ExportCommonIcons";
|
||||
import { getAllProjectsApi } from "../../services/dashboard/getAllProjectsApi";
|
||||
import { useSocketStore } from "../../store/socket/useSocketStore";
|
||||
import { Modal } from "../templates/PreviewModal";
|
||||
// import { viewProject } from "../../services/dashboard/viewProject";
|
||||
// import { updateProject } from "../../services/dashboard/updateProject";
|
||||
|
||||
@@ -25,8 +26,6 @@ interface DashBoardCardProps {
|
||||
handleDuplicateRecentProject?: (projectId: string, projectName: string, 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>>;
|
||||
@@ -52,8 +51,6 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
|
||||
handleDuplicateRecentProject,
|
||||
createdAt,
|
||||
createdBy,
|
||||
setRecentDuplicateData,
|
||||
setProjectDuplicateData,
|
||||
setActiveFolder,
|
||||
openKebabProjectId,
|
||||
setOpenKebabProjectId,
|
||||
@@ -68,6 +65,8 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
|
||||
const [renameValue, setRenameValue] = useState(projectName);
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const kebabRef = useRef<HTMLDivElement>(null);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [confirmText, setConfirmText] = useState("");
|
||||
|
||||
// Close kebab when clicking outside
|
||||
OuterClick({
|
||||
@@ -92,6 +91,12 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
|
||||
return kebabOptionsMap.default;
|
||||
}, [active, createdBy, userId]);
|
||||
|
||||
const handlePermanentDeleteConformation = () => {
|
||||
handleTrashDeleteProject?.(projectId);
|
||||
setShowDelete(false);
|
||||
setConfirmText("");
|
||||
};
|
||||
|
||||
const handleProjectName = useCallback(
|
||||
async (newName: string) => {
|
||||
setRenameValue(newName);
|
||||
@@ -123,7 +128,7 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
|
||||
async (option: string) => {
|
||||
switch (option) {
|
||||
case "delete":
|
||||
await (active === "trash" ? handleTrashDeleteProject?.(projectId) : handleDeleteProject?.(projectId));
|
||||
await (active === "trash" ? setShowDelete(true) : handleDeleteProject?.(projectId));
|
||||
break;
|
||||
case "restore":
|
||||
await handleRestoreProject?.(projectId);
|
||||
@@ -137,39 +142,17 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
|
||||
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,
|
||||
]
|
||||
[projectId, projectName, thumbnail, userId, active, handleDeleteProject, handleRestoreProject, handleDuplicateWorkspaceProject, handleDuplicateRecentProject, setProjectName, setActiveFolder]
|
||||
);
|
||||
|
||||
const getRelativeTime = useCallback((dateString: string): string => {
|
||||
@@ -280,6 +263,32 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
<Modal
|
||||
isOpen={showDelete}
|
||||
onClose={() => setShowDelete(false)}
|
||||
type="confirm"
|
||||
title="Delete Project"
|
||||
description={`This action is permanent. Please type ${projectName} to confirm.`}
|
||||
inputs={[
|
||||
{
|
||||
id: "confirmDelete",
|
||||
label: "Confirmation",
|
||||
placeholder: `Type ${projectName}`,
|
||||
value: confirmText,
|
||||
onChange: setConfirmText,
|
||||
},
|
||||
]}
|
||||
buttons={[
|
||||
{ label: "Cancel", onClick: () => setShowDelete(false), variant: "secondary" },
|
||||
{
|
||||
label: "Delete",
|
||||
onClick: handlePermanentDeleteConformation,
|
||||
variant: "danger",
|
||||
disabled: confirmText !== projectName,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react";
|
||||
import { getUserData } from "../../functions/getUserData";
|
||||
import { useSocketStore } from "../../store/socket/useSocketStore";
|
||||
|
||||
import { Modal } from "../templates/PreviewModal";
|
||||
import ProjectSocketRes from "./socket/projectSocketRes";
|
||||
import DashboardNavBar from "./DashboardNavBar";
|
||||
import DashboardCard from "./DashboardCard";
|
||||
@@ -24,59 +23,54 @@ interface DashboardMainProps {
|
||||
const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
|
||||
const [activeSubFolder, setActiveSubFolder] = useState("myProjects");
|
||||
const [projectsData, setProjectsData] = useState<DashboardProjectCollection>({});
|
||||
const [projectsCache, setProjectsCache] = useState<Record<string, DashboardProjectCollection>>({});
|
||||
const [isSearchActive, setIsSearchActive] = useState<boolean>(false);
|
||||
const [duplicateData, setDuplicateData] = useState<Object>({});
|
||||
const [openKebabProjectId, setOpenKebabProjectId] = useState<string | null>(null);
|
||||
const [projectsCache, setProjectsCache] = useState<{
|
||||
[key: string]: DashboardProjectCollection;
|
||||
}>({});
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [confirmText, setConfirmText] = useState(""); // 🔹 For confirmation input
|
||||
const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null); // 🔹 Store project id to delete
|
||||
|
||||
const { userId, organization } = getUserData();
|
||||
const { projectSocket } = useSocketStore();
|
||||
|
||||
// #region API Fetchers
|
||||
const fetchData = async () => {
|
||||
const cacheKey = activeFolder + (activeFolder === "projects" ? `-${activeSubFolder}` : "");
|
||||
|
||||
if (projectsCache[cacheKey] && !isSearchActive) {
|
||||
setProjectsData(projectsCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔹 Fetch all folders on mount and store locally
|
||||
const fetchAllData = async () => {
|
||||
try {
|
||||
let projects: DashboardProjectCollection = {};
|
||||
const [homeProjects, myProjects, sharedProjects, trashProjects] = await Promise.all([recentlyViewedApi(), getAllProjectsApi(), sharedWithMeProjectsApi(), getTrashApi()]);
|
||||
|
||||
switch (activeFolder) {
|
||||
case "home":
|
||||
projects = await recentlyViewedApi();
|
||||
break;
|
||||
case "projects":
|
||||
if (activeSubFolder === "myProjects") {
|
||||
projects = await getAllProjectsApi();
|
||||
} else {
|
||||
projects = await sharedWithMeProjectsApi();
|
||||
}
|
||||
break;
|
||||
case "trash":
|
||||
projects = await getTrashApi();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
const cache: Record<string, DashboardProjectCollection> = {
|
||||
home: homeProjects,
|
||||
"projects-myProjects": myProjects,
|
||||
"projects-shared": sharedProjects,
|
||||
trash: trashProjects,
|
||||
};
|
||||
|
||||
if (projects && JSON.stringify(projects) !== JSON.stringify(projectsData)) {
|
||||
setProjectsCache((prev) => ({ ...prev, [cacheKey]: projects }));
|
||||
setProjectsData(projects);
|
||||
setProjectsCache(cache);
|
||||
|
||||
// set current active data
|
||||
const cacheKey = activeFolder === "projects" ? `projects-${activeSubFolder}` : activeFolder;
|
||||
if (cache[cacheKey]) {
|
||||
setProjectsData(cache[cacheKey]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error("Failed to fetch dashboard data:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// #region Search Handlers
|
||||
// 🔹 On mount fetch once
|
||||
useEffect(() => {
|
||||
fetchAllData();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 🔹 Switch folder/subFolder from cache
|
||||
useEffect(() => {
|
||||
if (!isSearchActive) {
|
||||
const cacheKey = activeFolder === "projects" ? `projects-${activeSubFolder}` : activeFolder;
|
||||
if (projectsCache[cacheKey]) {
|
||||
setProjectsData(projectsCache[cacheKey]);
|
||||
}
|
||||
}
|
||||
}, [activeFolder, activeSubFolder, isSearchActive, projectsCache]);
|
||||
|
||||
// 🔹 Search handler
|
||||
const handleSearch = async (inputValue: string) => {
|
||||
if (!inputValue.trim()) {
|
||||
setIsSearchActive(false);
|
||||
@@ -94,21 +88,43 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
|
||||
setProjectsData(results?.message ? {} : results);
|
||||
};
|
||||
|
||||
// #region Socket Actions
|
||||
const handleDelete = async (projectId: string): Promise<void> => {
|
||||
if (projectSocket) {
|
||||
projectSocket.emit("v1:project:delete", {
|
||||
projectId,
|
||||
organization,
|
||||
userId,
|
||||
// 🔹 Socket Actions
|
||||
const updateStateAfterRemove = (projectId: string) => {
|
||||
setProjectsData((prev: DashboardProjectCollection) => {
|
||||
const key = Object.keys(prev)[0];
|
||||
const updatedList = prev[key]?.filter((p) => p._id !== projectId) || [];
|
||||
return { ...prev, [key]: updatedList };
|
||||
});
|
||||
}
|
||||
updateStateAfterRemove(projectId);
|
||||
|
||||
setProjectsCache((prev) => {
|
||||
const newCache = { ...prev };
|
||||
Object.keys(newCache).forEach((k) => {
|
||||
const key = Object.keys(newCache[k])[0];
|
||||
newCache[k] = {
|
||||
...newCache[k],
|
||||
[key]: newCache[k][key]?.filter((p) => p._id !== projectId) || [],
|
||||
};
|
||||
});
|
||||
return newCache;
|
||||
});
|
||||
|
||||
setIsSearchActive(false);
|
||||
};
|
||||
|
||||
const handlePermanentDelete = async (): Promise<void> => {
|
||||
if (!deleteTargetId) return;
|
||||
const handleDelete = async (projectId: string): Promise<void> => {
|
||||
if (projectSocket) {
|
||||
projectSocket.emit("v1:project:delete", { projectId, organization, userId });
|
||||
}
|
||||
updateStateAfterRemove(projectId);
|
||||
|
||||
// 🔹 Refresh trash folder cache (since project moves there)
|
||||
const trashProjects = await getTrashApi();
|
||||
setProjectsCache((prev) => ({ ...prev, trash: trashProjects }));
|
||||
};
|
||||
|
||||
const handlePermanentDelete = async (deleteTargetId: string): Promise<void> => {
|
||||
console.log("deleteTargetId: ", deleteTargetId);
|
||||
if (!deleteTargetId) return;
|
||||
if (projectSocket) {
|
||||
projectSocket.emit("v1:trash:delete", {
|
||||
projectId: deleteTargetId,
|
||||
@@ -117,14 +133,24 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
|
||||
});
|
||||
}
|
||||
updateStateAfterRemove(deleteTargetId);
|
||||
setShowDelete(false);
|
||||
setConfirmText("");
|
||||
setDeleteTargetId(null);
|
||||
|
||||
// 🔹 Refresh trash folder
|
||||
const trashProjects = await getTrashApi();
|
||||
console.log("trashProjects: ", trashProjects);
|
||||
setProjectsCache((prev) => ({ ...prev, trash: trashProjects }));
|
||||
};
|
||||
|
||||
const handleRestore = async (projectId: string): Promise<void> => {
|
||||
await restoreTrashApi(projectId);
|
||||
updateStateAfterRemove(projectId);
|
||||
|
||||
// 🔹 Refresh home & projects-myProjects
|
||||
const [homeProjects, myProjects] = await Promise.all([recentlyViewedApi(), getAllProjectsApi()]);
|
||||
setProjectsCache((prev) => ({
|
||||
...prev,
|
||||
home: homeProjects,
|
||||
"projects-myProjects": myProjects,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleDuplicate = async (projectId: string, projectName: string, thumbnail: string): Promise<void> => {
|
||||
@@ -138,18 +164,11 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
|
||||
projectName,
|
||||
});
|
||||
}
|
||||
// 🔹 Refresh home & projects cache
|
||||
await fetchAllData();
|
||||
};
|
||||
|
||||
// #region Project Map
|
||||
const updateStateAfterRemove = (projectId: string) => {
|
||||
setProjectsData((prev: DashboardProjectCollection) => {
|
||||
const key = Object.keys(prev)[0];
|
||||
const updatedList = prev[key]?.filter((p) => p._id !== projectId) || [];
|
||||
return { ...prev, [key]: updatedList };
|
||||
});
|
||||
setIsSearchActive(false);
|
||||
};
|
||||
|
||||
// 🔹 Render Projects
|
||||
const renderProjects = () => {
|
||||
const key = Object.keys(projectsData)[0];
|
||||
const projectList = projectsData[key];
|
||||
@@ -169,23 +188,19 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
|
||||
{...(activeFolder === "home" && {
|
||||
handleDeleteProject: handleDelete,
|
||||
handleDuplicateRecentProject: handleDuplicate,
|
||||
setRecentDuplicateData: setDuplicateData,
|
||||
})}
|
||||
{...(activeSubFolder === "myProjects" && {
|
||||
handleDeleteProject: handleDelete,
|
||||
handleDuplicateWorkspaceProject: handleDuplicate,
|
||||
setProjectDuplicateData: setDuplicateData,
|
||||
})}
|
||||
{...(activeSubFolder === "shared" && {
|
||||
handleDuplicateWorkspaceProject: handleDuplicate,
|
||||
setProjectDuplicateData: setDuplicateData,
|
||||
active: "shared",
|
||||
})}
|
||||
{...(activeFolder === "trash" && {
|
||||
handleRestoreProject: handleRestore,
|
||||
handleTrashDeleteProject: async (id: string): Promise<void> => {
|
||||
setDeleteTargetId(id);
|
||||
setShowDelete(true);
|
||||
handlePermanentDelete(id);
|
||||
},
|
||||
active: "trash",
|
||||
})}
|
||||
@@ -195,12 +210,6 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
|
||||
));
|
||||
};
|
||||
|
||||
// #region Use Effects
|
||||
useEffect(() => {
|
||||
if (!isSearchActive) fetchData();
|
||||
// eslint-disable-next-line
|
||||
}, [activeFolder, isSearchActive, activeSubFolder]);
|
||||
|
||||
return (
|
||||
<div className="dashboard-home-container">
|
||||
<DashboardNavBar
|
||||
@@ -231,33 +240,6 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
|
||||
|
||||
<ProjectSocketRes setIsSearchActive={setIsSearchActive} {...(activeFolder === "home" ? { setRecentProjects: setProjectsData } : { setWorkspaceProjects: setProjectsData })} />
|
||||
</div>
|
||||
|
||||
{/* 🔹 Delete Confirmation Modal */}
|
||||
<Modal
|
||||
isOpen={showDelete}
|
||||
onClose={() => 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",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,38 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import DashboardNavBar from "./DashboardNavBar";
|
||||
import { projectTutorialApi } from "../../services/dashboard/projectTutorialApi";
|
||||
import { AIIcon } from "../icons/ExportCommonIcons";
|
||||
import { DeleteIcon } from "../icons/ContextMenuIcons";
|
||||
|
||||
interface Tutorial {
|
||||
_id: string;
|
||||
name: string;
|
||||
thumbnail?: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const TutorialCard: React.FC<{ tutorial: Tutorial }> = ({ tutorial }) => {
|
||||
return (
|
||||
<div className="tutorial-card-container">
|
||||
<div
|
||||
className="thumbnail"
|
||||
style={{
|
||||
backgroundImage: tutorial.thumbnail
|
||||
? `url(${tutorial.thumbnail})`
|
||||
: "linear-gradient(135deg, #ddd, #bbb)",
|
||||
}}
|
||||
></div>
|
||||
<div className="tutorial-details">
|
||||
<div className="tutorial-name">{tutorial.name}</div>
|
||||
<div className="updated-date">
|
||||
{new Date(tutorial.updatedAt).toLocaleDateString()}
|
||||
</div>
|
||||
<div className="delete-option">
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { TutorialCard } from "./TutorialCard";
|
||||
|
||||
const DashboardTutorial: React.FC = () => {
|
||||
const [tutorials, setTutorials] = useState<Tutorial[]>([]);
|
||||
@@ -65,7 +34,6 @@ const DashboardTutorial: React.FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tutorials-list">
|
||||
{tutorials.length > 0 ? (
|
||||
tutorials.map((tut) => <TutorialCard key={tut._id} tutorial={tut} />)
|
||||
|
||||
@@ -86,6 +86,18 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
|
||||
<TrashIcon isActive={activeTab === "Trash"} />
|
||||
Trash
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === "Use Cases" ? "option-list active" : "option-list"}
|
||||
title="use case"
|
||||
// disabled
|
||||
onClick={() => {
|
||||
setActiveTab("Use Cases");
|
||||
console.warn("Use Cases comming soon");
|
||||
}}
|
||||
>
|
||||
<TutorialsIcon isActive={activeTab === "Use Cases"} />
|
||||
Use Cases
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === "Tutorials" ? "option-list active" : "option-list"}
|
||||
title="coming soon"
|
||||
|
||||
32
app/src/components/Dashboard/TutorialCard.tsx
Normal file
32
app/src/components/Dashboard/TutorialCard.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { getUserData } from "../../functions/getUserData";
|
||||
import { ALPHA_ORG } from "../../pages/Dashboard";
|
||||
import { DeleteIcon } from "../icons/ContextMenuIcons";
|
||||
|
||||
export const TutorialCard: React.FC<{ tutorial: Tutorial }> = ({ tutorial }) => {
|
||||
const { organization } = getUserData();
|
||||
return (
|
||||
<div className="tutorial-card-container">
|
||||
<div
|
||||
className="preview-container"
|
||||
style={{
|
||||
backgroundImage: tutorial.thumbnail
|
||||
? `url(${tutorial.thumbnail})`
|
||||
: "linear-gradient(135deg, #ddd, #bbb)",
|
||||
}}
|
||||
></div>
|
||||
<div className="tutorial-details">
|
||||
<div className="context">
|
||||
<div className="tutorial-name">{tutorial.name}</div>
|
||||
<div className="updated-date">
|
||||
{new Date(tutorial.updatedAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
{organization === ALPHA_ORG && (
|
||||
<div className="delete-option">
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { DepthOfField, Bloom, EffectComposer, N8AO } from "@react-three/postprocessing";
|
||||
import { EffectComposer, N8AO } from "@react-three/postprocessing";
|
||||
import OutlineInstances from "./outlineInstances/outlineInstances";
|
||||
import { useDeletableEventSphere, useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";
|
||||
|
||||
|
||||
@@ -284,25 +284,25 @@ function RoboticArmInstance({ armBot }: { readonly armBot: ArmBotStatus }) {
|
||||
}
|
||||
|
||||
const HandleCallback = () => {
|
||||
if (armBot.isActive && armBot.state == "running" && currentPhase == "init-to-rest") {
|
||||
if (armBot.isActive && armBot.state === "running" && currentPhase === "init-to-rest") {
|
||||
logStatus(armBot.modelUuid, "Callback triggered: rest");
|
||||
setArmBotActive(armBot.modelUuid, false);
|
||||
setArmBotState(armBot.modelUuid, "idle");
|
||||
setCurrentPhase("rest");
|
||||
setPath([]);
|
||||
} else if (armBot.state == "running" && currentPhase == "rest-to-start") {
|
||||
} else if (armBot.state === "running" && currentPhase === "rest-to-start") {
|
||||
logStatus(armBot.modelUuid, "Callback triggered: pick.");
|
||||
setArmBotActive(armBot.modelUuid, false);
|
||||
setArmBotState(armBot.modelUuid, "running");
|
||||
setCurrentPhase("picking");
|
||||
setPath([]);
|
||||
} else if (armBot.isActive && armBot.state == "running" && currentPhase == "start-to-end") {
|
||||
} else if (armBot.isActive && armBot.state === "running" && currentPhase === "start-to-end") {
|
||||
logStatus(armBot.modelUuid, "Callback triggered: drop.");
|
||||
setArmBotActive(armBot.modelUuid, false);
|
||||
setArmBotState(armBot.modelUuid, "running");
|
||||
setCurrentPhase("dropping");
|
||||
setPath([]);
|
||||
} else if (armBot.isActive && armBot.state == "running" && currentPhase == "end-to-rest") {
|
||||
} else if (armBot.isActive && armBot.state === "running" && currentPhase === "end-to-rest") {
|
||||
logStatus(armBot.modelUuid, "Callback triggered: rest, cycle completed.");
|
||||
setArmBotActive(armBot.modelUuid, false);
|
||||
setArmBotState(armBot.modelUuid, "idle");
|
||||
|
||||
@@ -4,6 +4,9 @@ import { getUserData } from "../functions/getUserData";
|
||||
import SidePannel from "../components/Dashboard/SidePannel";
|
||||
import DashboardTutorial from "../components/Dashboard/DashboardTutorial";
|
||||
import DashboardMain from "../components/Dashboard/DashboardMain";
|
||||
import DashboardUseCases from "../components/Dashboard/DasboardUseCases";
|
||||
|
||||
export const ALPHA_ORG = "hexrfactory";
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>("Home");
|
||||
@@ -22,6 +25,7 @@ const Dashboard: React.FC = () => {
|
||||
<SidePannel setActiveTab={setActiveTab} activeTab={activeTab} />
|
||||
{["Home", "Projects", "Shared", "Trash"].includes(activeTab) && <DashboardMain activeFolder={activeTab.toLowerCase() as Folder} />}
|
||||
{activeTab === "Tutorials" && <DashboardTutorial />}
|
||||
{activeTab === "Use Cases" && <DashboardUseCases />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -195,7 +195,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cards-container {
|
||||
.cards-container,
|
||||
.tutorials-list {
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
@@ -210,7 +211,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-card-container {
|
||||
.dashboard-card-container,
|
||||
.tutorial-card-container {
|
||||
height: 242px;
|
||||
width: calc((100% / 5) - 23px);
|
||||
min-width: 260px;
|
||||
@@ -400,3 +402,97 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tutorials
|
||||
.tutorials-list {
|
||||
.tutorials-main-header {
|
||||
position: relative;
|
||||
height: 242px;
|
||||
width: calc((100% / 5) - 23px);
|
||||
min-width: 260px;
|
||||
border-radius: 22px;
|
||||
.tutorial-buttons-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
.add-tutorials-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 24px;
|
||||
background: var(--background-color);
|
||||
transition: background 0.2s;
|
||||
span {
|
||||
font-size: 0.84rem;
|
||||
font-size: 8rem;
|
||||
color: var(--text-disabled);
|
||||
transform: translateY(-8px);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
&:hover {
|
||||
background: var(--background-color-selected);
|
||||
span {
|
||||
color: var(--text-button);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tutorial-card-container {
|
||||
overflow: hidden;
|
||||
.preview-container {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
&:hover {
|
||||
.tutorial-details {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
.tutorial-details {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
background: var(--background-color);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 16px;
|
||||
transform: translateY(100%);
|
||||
transition: all 0.2s;
|
||||
.context {
|
||||
.tutorial-name {
|
||||
color: var(--text-color);
|
||||
}
|
||||
.updated-date {
|
||||
color: var(--input-text-color);
|
||||
}
|
||||
}
|
||||
.delete-option {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
background: var(--background-color-solid);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
transition: background 0.2s;
|
||||
&:hover {
|
||||
background: var(--log-error-background-color);
|
||||
path {
|
||||
stroke: var(--log-error-text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
app/src/types/uiTypes.d.ts
vendored
7
app/src/types/uiTypes.d.ts
vendored
@@ -70,3 +70,10 @@ interface ListProps {
|
||||
items?: ZoneItem[];
|
||||
remove?: boolean;
|
||||
}
|
||||
|
||||
interface Tutorial {
|
||||
_id: string;
|
||||
name: string;
|
||||
thumbnail?: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user