Merge remote-tracking branch 'origin/main-demo' into main-dev

This commit is contained in:
2025-09-16 16:46:43 +05:30
11 changed files with 721 additions and 535 deletions

View 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;

View File

@@ -8,6 +8,7 @@ import OuterClick from "../../utils/outerClick";
import { KebabIcon } from "../icons/ExportCommonIcons"; import { KebabIcon } from "../icons/ExportCommonIcons";
import { getAllProjectsApi } from "../../services/dashboard/getAllProjectsApi"; import { getAllProjectsApi } from "../../services/dashboard/getAllProjectsApi";
import { useSocketStore } from "../../store/socket/useSocketStore"; import { useSocketStore } from "../../store/socket/useSocketStore";
import { Modal } from "../templates/PreviewModal";
// import { viewProject } from "../../services/dashboard/viewProject"; // import { viewProject } from "../../services/dashboard/viewProject";
// import { updateProject } from "../../services/dashboard/updateProject"; // import { updateProject } from "../../services/dashboard/updateProject";
@@ -25,8 +26,6 @@ interface DashBoardCardProps {
handleDuplicateRecentProject?: (projectId: string, projectName: string, thumbnail: string) => Promise<void>; handleDuplicateRecentProject?: (projectId: string, projectName: string, thumbnail: string) => Promise<void>;
active?: "shared" | "trash" | "recent" | string; active?: "shared" | "trash" | "recent" | string;
setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>; setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>;
setRecentDuplicateData?: React.Dispatch<React.SetStateAction<object>>;
setProjectDuplicateData?: React.Dispatch<React.SetStateAction<object>>;
setActiveFolder?: React.Dispatch<React.SetStateAction<string>>; setActiveFolder?: React.Dispatch<React.SetStateAction<string>>;
openKebabProjectId: string | null; openKebabProjectId: string | null;
setOpenKebabProjectId: React.Dispatch<React.SetStateAction<string | null>>; setOpenKebabProjectId: React.Dispatch<React.SetStateAction<string | null>>;
@@ -52,8 +51,6 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
handleDuplicateRecentProject, handleDuplicateRecentProject,
createdAt, createdAt,
createdBy, createdBy,
setRecentDuplicateData,
setProjectDuplicateData,
setActiveFolder, setActiveFolder,
openKebabProjectId, openKebabProjectId,
setOpenKebabProjectId, setOpenKebabProjectId,
@@ -68,6 +65,8 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
const [renameValue, setRenameValue] = useState(projectName); const [renameValue, setRenameValue] = useState(projectName);
const [isRenaming, setIsRenaming] = useState(false); const [isRenaming, setIsRenaming] = useState(false);
const kebabRef = useRef<HTMLDivElement>(null); const kebabRef = useRef<HTMLDivElement>(null);
const [showDelete, setShowDelete] = useState(false);
const [confirmText, setConfirmText] = useState("");
// Close kebab when clicking outside // Close kebab when clicking outside
OuterClick({ OuterClick({
@@ -92,6 +91,12 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
return kebabOptionsMap.default; return kebabOptionsMap.default;
}, [active, createdBy, userId]); }, [active, createdBy, userId]);
const handlePermanentDeleteConformation = () => {
handleTrashDeleteProject?.(projectId);
setShowDelete(false);
setConfirmText("");
};
const handleProjectName = useCallback( const handleProjectName = useCallback(
async (newName: string) => { async (newName: string) => {
setRenameValue(newName); setRenameValue(newName);
@@ -123,7 +128,7 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
async (option: string) => { async (option: string) => {
switch (option) { switch (option) {
case "delete": case "delete":
await (active === "trash" ? handleTrashDeleteProject?.(projectId) : handleDeleteProject?.(projectId)); await (active === "trash" ? setShowDelete(true) : handleDeleteProject?.(projectId));
break; break;
case "restore": case "restore":
await handleRestoreProject?.(projectId); await handleRestoreProject?.(projectId);
@@ -137,39 +142,17 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
break; break;
case "duplicate": case "duplicate":
if (handleDuplicateWorkspaceProject) { if (handleDuplicateWorkspaceProject) {
setProjectDuplicateData?.({ projectId, projectName, thumbnail });
await handleDuplicateWorkspaceProject(projectId, projectName, thumbnail, userId); await handleDuplicateWorkspaceProject(projectId, projectName, thumbnail, userId);
if (active === "shared") { if (active === "shared") {
setActiveFolder?.("myProjects"); setActiveFolder?.("myProjects");
} }
} else if (handleDuplicateRecentProject) { } else if (handleDuplicateRecentProject) {
setRecentDuplicateData?.({
projectId,
projectName,
thumbnail,
userId,
});
await handleDuplicateRecentProject(projectId, projectName, thumbnail); await handleDuplicateRecentProject(projectId, projectName, thumbnail);
} }
break; break;
} }
}, },
[ [projectId, projectName, thumbnail, userId, active, handleDeleteProject, handleRestoreProject, handleDuplicateWorkspaceProject, handleDuplicateRecentProject, setProjectName, setActiveFolder]
projectId,
projectName,
thumbnail,
userId,
active,
handleDeleteProject,
handleTrashDeleteProject,
handleRestoreProject,
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
setProjectName,
setProjectDuplicateData,
setRecentDuplicateData,
setActiveFolder,
]
); );
const getRelativeTime = useCallback((dateString: string): string => { const getRelativeTime = useCallback((dateString: string): string => {
@@ -280,6 +263,32 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
</div>, </div>,
document.body 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> </div>
); );
}; };

View File

@@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react";
import { getUserData } from "../../functions/getUserData"; import { getUserData } from "../../functions/getUserData";
import { useSocketStore } from "../../store/socket/useSocketStore"; import { useSocketStore } from "../../store/socket/useSocketStore";
import { Modal } from "../templates/PreviewModal";
import ProjectSocketRes from "./socket/projectSocketRes"; import ProjectSocketRes from "./socket/projectSocketRes";
import DashboardNavBar from "./DashboardNavBar"; import DashboardNavBar from "./DashboardNavBar";
import DashboardCard from "./DashboardCard"; import DashboardCard from "./DashboardCard";
@@ -24,59 +23,54 @@ interface DashboardMainProps {
const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => { const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
const [activeSubFolder, setActiveSubFolder] = useState("myProjects"); const [activeSubFolder, setActiveSubFolder] = useState("myProjects");
const [projectsData, setProjectsData] = useState<DashboardProjectCollection>({}); const [projectsData, setProjectsData] = useState<DashboardProjectCollection>({});
const [projectsCache, setProjectsCache] = useState<Record<string, DashboardProjectCollection>>({});
const [isSearchActive, setIsSearchActive] = useState<boolean>(false); const [isSearchActive, setIsSearchActive] = useState<boolean>(false);
const [duplicateData, setDuplicateData] = useState<Object>({});
const [openKebabProjectId, setOpenKebabProjectId] = useState<string | null>(null); 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 { userId, organization } = getUserData();
const { projectSocket } = useSocketStore(); const { projectSocket } = useSocketStore();
// #region API Fetchers // 🔹 Fetch all folders on mount and store locally
const fetchData = async () => { const fetchAllData = async () => {
const cacheKey = activeFolder + (activeFolder === "projects" ? `-${activeSubFolder}` : "");
if (projectsCache[cacheKey] && !isSearchActive) {
setProjectsData(projectsCache[cacheKey]);
return;
}
try { try {
let projects: DashboardProjectCollection = {}; const [homeProjects, myProjects, sharedProjects, trashProjects] = await Promise.all([recentlyViewedApi(), getAllProjectsApi(), sharedWithMeProjectsApi(), getTrashApi()]);
switch (activeFolder) { const cache: Record<string, DashboardProjectCollection> = {
case "home": home: homeProjects,
projects = await recentlyViewedApi(); "projects-myProjects": myProjects,
break; "projects-shared": sharedProjects,
case "projects": trash: trashProjects,
if (activeSubFolder === "myProjects") { };
projects = await getAllProjectsApi();
} else {
projects = await sharedWithMeProjectsApi();
}
break;
case "trash":
projects = await getTrashApi();
break;
default:
return;
}
if (projects && JSON.stringify(projects) !== JSON.stringify(projectsData)) { setProjectsCache(cache);
setProjectsCache((prev) => ({ ...prev, [cacheKey]: projects }));
setProjectsData(projects); // set current active data
const cacheKey = activeFolder === "projects" ? `projects-${activeSubFolder}` : activeFolder;
if (cache[cacheKey]) {
setProjectsData(cache[cacheKey]);
} }
} catch (error) { } 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) => { const handleSearch = async (inputValue: string) => {
if (!inputValue.trim()) { if (!inputValue.trim()) {
setIsSearchActive(false); setIsSearchActive(false);
@@ -94,21 +88,43 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
setProjectsData(results?.message ? {} : results); setProjectsData(results?.message ? {} : results);
}; };
// #region Socket Actions // 🔹 Socket Actions
const handleDelete = async (projectId: string): Promise<void> => { const updateStateAfterRemove = (projectId: string) => {
if (projectSocket) { setProjectsData((prev: DashboardProjectCollection) => {
projectSocket.emit("v1:project:delete", { const key = Object.keys(prev)[0];
projectId, const updatedList = prev[key]?.filter((p) => p._id !== projectId) || [];
organization, return { ...prev, [key]: updatedList };
userId, });
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;
updateStateAfterRemove(projectId); });
setIsSearchActive(false);
}; };
const handlePermanentDelete = async (): Promise<void> => { const handleDelete = async (projectId: string): Promise<void> => {
if (!deleteTargetId) return; 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) { if (projectSocket) {
projectSocket.emit("v1:trash:delete", { projectSocket.emit("v1:trash:delete", {
projectId: deleteTargetId, projectId: deleteTargetId,
@@ -117,14 +133,24 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
}); });
} }
updateStateAfterRemove(deleteTargetId); updateStateAfterRemove(deleteTargetId);
setShowDelete(false);
setConfirmText(""); // 🔹 Refresh trash folder
setDeleteTargetId(null); const trashProjects = await getTrashApi();
console.log("trashProjects: ", trashProjects);
setProjectsCache((prev) => ({ ...prev, trash: trashProjects }));
}; };
const handleRestore = async (projectId: string): Promise<void> => { const handleRestore = async (projectId: string): Promise<void> => {
await restoreTrashApi(projectId); await restoreTrashApi(projectId);
updateStateAfterRemove(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> => { const handleDuplicate = async (projectId: string, projectName: string, thumbnail: string): Promise<void> => {
@@ -138,18 +164,11 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
projectName, projectName,
}); });
} }
// 🔹 Refresh home & projects cache
await fetchAllData();
}; };
// #region Project Map // 🔹 Render Projects
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);
};
const renderProjects = () => { const renderProjects = () => {
const key = Object.keys(projectsData)[0]; const key = Object.keys(projectsData)[0];
const projectList = projectsData[key]; const projectList = projectsData[key];
@@ -169,23 +188,19 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
{...(activeFolder === "home" && { {...(activeFolder === "home" && {
handleDeleteProject: handleDelete, handleDeleteProject: handleDelete,
handleDuplicateRecentProject: handleDuplicate, handleDuplicateRecentProject: handleDuplicate,
setRecentDuplicateData: setDuplicateData,
})} })}
{...(activeSubFolder === "myProjects" && { {...(activeSubFolder === "myProjects" && {
handleDeleteProject: handleDelete, handleDeleteProject: handleDelete,
handleDuplicateWorkspaceProject: handleDuplicate, handleDuplicateWorkspaceProject: handleDuplicate,
setProjectDuplicateData: setDuplicateData,
})} })}
{...(activeSubFolder === "shared" && { {...(activeSubFolder === "shared" && {
handleDuplicateWorkspaceProject: handleDuplicate, handleDuplicateWorkspaceProject: handleDuplicate,
setProjectDuplicateData: setDuplicateData,
active: "shared", active: "shared",
})} })}
{...(activeFolder === "trash" && { {...(activeFolder === "trash" && {
handleRestoreProject: handleRestore, handleRestoreProject: handleRestore,
handleTrashDeleteProject: async (id: string): Promise<void> => { handleTrashDeleteProject: async (id: string): Promise<void> => {
setDeleteTargetId(id); handlePermanentDelete(id);
setShowDelete(true);
}, },
active: "trash", 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 ( return (
<div className="dashboard-home-container"> <div className="dashboard-home-container">
<DashboardNavBar <DashboardNavBar
@@ -231,33 +240,6 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
<ProjectSocketRes setIsSearchActive={setIsSearchActive} {...(activeFolder === "home" ? { setRecentProjects: setProjectsData } : { setWorkspaceProjects: setProjectsData })} /> <ProjectSocketRes setIsSearchActive={setIsSearchActive} {...(activeFolder === "home" ? { setRecentProjects: setProjectsData } : { setWorkspaceProjects: setProjectsData })} />
</div> </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> </div>
); );
}; };

View File

@@ -2,38 +2,7 @@ import React, { useEffect, useState } from "react";
import DashboardNavBar from "./DashboardNavBar"; import DashboardNavBar from "./DashboardNavBar";
import { projectTutorialApi } from "../../services/dashboard/projectTutorialApi"; import { projectTutorialApi } from "../../services/dashboard/projectTutorialApi";
import { AIIcon } from "../icons/ExportCommonIcons"; import { AIIcon } from "../icons/ExportCommonIcons";
import { DeleteIcon } from "../icons/ContextMenuIcons"; import { TutorialCard } from "./TutorialCard";
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>
);
};
const DashboardTutorial: React.FC = () => { const DashboardTutorial: React.FC = () => {
const [tutorials, setTutorials] = useState<Tutorial[]>([]); const [tutorials, setTutorials] = useState<Tutorial[]>([]);
@@ -65,7 +34,6 @@ const DashboardTutorial: React.FC = () => {
</button> </button>
</div> </div>
</div> </div>
<div className="tutorials-list"> <div className="tutorials-list">
{tutorials.length > 0 ? ( {tutorials.length > 0 ? (
tutorials.map((tut) => <TutorialCard key={tut._id} tutorial={tut} />) tutorials.map((tut) => <TutorialCard key={tut._id} tutorial={tut} />)

View File

@@ -86,6 +86,18 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
<TrashIcon isActive={activeTab === "Trash"} /> <TrashIcon isActive={activeTab === "Trash"} />
Trash Trash
</button> </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 <button
className={activeTab === "Tutorials" ? "option-list active" : "option-list"} className={activeTab === "Tutorials" ? "option-list active" : "option-list"}
title="coming soon" title="coming soon"

View 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>
);
};

View File

@@ -1,5 +1,5 @@
import { useEffect } from "react"; 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 OutlineInstances from "./outlineInstances/outlineInstances";
import { useDeletableEventSphere, useSelectedEventSphere } from "../../../store/simulation/useSimulationStore"; import { useDeletableEventSphere, useSelectedEventSphere } from "../../../store/simulation/useSimulationStore";

View File

@@ -284,25 +284,25 @@ function RoboticArmInstance({ armBot }: { readonly armBot: ArmBotStatus }) {
} }
const HandleCallback = () => { 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"); logStatus(armBot.modelUuid, "Callback triggered: rest");
setArmBotActive(armBot.modelUuid, false); setArmBotActive(armBot.modelUuid, false);
setArmBotState(armBot.modelUuid, "idle"); setArmBotState(armBot.modelUuid, "idle");
setCurrentPhase("rest"); setCurrentPhase("rest");
setPath([]); 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."); logStatus(armBot.modelUuid, "Callback triggered: pick.");
setArmBotActive(armBot.modelUuid, false); setArmBotActive(armBot.modelUuid, false);
setArmBotState(armBot.modelUuid, "running"); setArmBotState(armBot.modelUuid, "running");
setCurrentPhase("picking"); setCurrentPhase("picking");
setPath([]); 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."); logStatus(armBot.modelUuid, "Callback triggered: drop.");
setArmBotActive(armBot.modelUuid, false); setArmBotActive(armBot.modelUuid, false);
setArmBotState(armBot.modelUuid, "running"); setArmBotState(armBot.modelUuid, "running");
setCurrentPhase("dropping"); setCurrentPhase("dropping");
setPath([]); 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."); logStatus(armBot.modelUuid, "Callback triggered: rest, cycle completed.");
setArmBotActive(armBot.modelUuid, false); setArmBotActive(armBot.modelUuid, false);
setArmBotState(armBot.modelUuid, "idle"); setArmBotState(armBot.modelUuid, "idle");

View File

@@ -4,6 +4,9 @@ import { getUserData } from "../functions/getUserData";
import SidePannel from "../components/Dashboard/SidePannel"; import SidePannel from "../components/Dashboard/SidePannel";
import DashboardTutorial from "../components/Dashboard/DashboardTutorial"; import DashboardTutorial from "../components/Dashboard/DashboardTutorial";
import DashboardMain from "../components/Dashboard/DashboardMain"; import DashboardMain from "../components/Dashboard/DashboardMain";
import DashboardUseCases from "../components/Dashboard/DasboardUseCases";
export const ALPHA_ORG = "hexrfactory";
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>("Home"); const [activeTab, setActiveTab] = useState<string>("Home");
@@ -22,6 +25,7 @@ const Dashboard: React.FC = () => {
<SidePannel setActiveTab={setActiveTab} activeTab={activeTab} /> <SidePannel setActiveTab={setActiveTab} activeTab={activeTab} />
{["Home", "Projects", "Shared", "Trash"].includes(activeTab) && <DashboardMain activeFolder={activeTab.toLowerCase() as Folder} />} {["Home", "Projects", "Shared", "Trash"].includes(activeTab) && <DashboardMain activeFolder={activeTab.toLowerCase() as Folder} />}
{activeTab === "Tutorials" && <DashboardTutorial />} {activeTab === "Tutorials" && <DashboardTutorial />}
{activeTab === "Use Cases" && <DashboardUseCases />}
</div> </div>
); );
}; };

View File

@@ -2,401 +2,497 @@
@use "../abstracts/mixins.scss" as *; @use "../abstracts/mixins.scss" as *;
.dashboard-main { .dashboard-main {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
display: flex;
padding: 27px 17px;
.side-pannel-container {
padding: 32px;
min-width: 280px;
height: 100%;
display: flex; display: flex;
flex-direction: column; padding: 27px 17px;
gap: 16px;
background: var(--background-color);
backdrop-filter: blur(20px);
border-radius: 30px;
box-shadow: var(--box-shadow-medium);
.side-pannel-header { .side-pannel-container {
@include flex-space-between; padding: 32px;
min-width: 280px;
.user-container { height: 100%;
@include flex-center;
gap: 6px;
.user-profile {
height: 32px;
width: 32px;
line-height: 32px;
text-align: center;
font-weight: var(--font-weight-medium);
background: var(--background-color-accent);
color: var(--text-button-color);
border-radius: #{$border-radius-circle};
}
.user-name {
color: var(--accent-color);
}
}
.notifications-container {
@include flex-center;
height: 24px;
width: 24px;
cursor: pointer;
}
}
.new-project-button {
position: relative;
padding: 12px 16px;
color: var(--text-color);
background: #7b4cd323;
border-radius: #{$border-radius-xxx};
overflow: hidden;
cursor: pointer;
transition: color 0.3s;
&::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, -50%);
height: 0px;
width: 0px;
border-radius: 50%;
background: var(--background-color-accent);
z-index: -1;
transition: all 0.25s ease-in-out;
}
&:hover {
color: var(--text-button-color);
&::after {
height: 260px;
width: 260px;
left: 50%;
scale: 1;
}
}
}
.side-bar-content-container {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
.side-bar-options-container {
.option-list {
display: flex;
position: relative;
align-items: center;
gap: 8px;
padding: 8px 10px;
margin: 4px 0;
border-radius: #{$border-radius-extra-large};
width: 100%;
overflow: hidden;
cursor: pointer;
transition: color 0.3s;
&::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, -50%);
height: 0px;
width: 0px;
border-radius: 50%;
background: #7b4cd323;
z-index: -1;
transition: all 0.5s ease-in-out;
}
&:disabled {
cursor: help;
}
&:hover {
&::after {
height: 260px;
width: 260px;
left: 50%;
scale: 1;
}
}
}
.active {
color: var(--text-button-color);
font-weight: var(--font-weight-medium);
background: var(--background-color-button);
&:hover {
background: var(--background-color-button);
}
}
}
}
}
.dashboard-home-container {
width: 100%;
padding-left: 18px;
.dashboard-navbar-container {
margin-top: 28px;
margin-bottom: 22px;
@include flex-center;
.title {
text-transform: capitalize;
font-size: var(--font-size-large);
width: 100%;
}
.market-place-button {
@include flex-center;
gap: 6px;
padding: 8px 14px;
background: var(--background-color-button);
white-space: nowrap;
border-radius: #{$border-radius-extra-large};
color: var(--text-button-color);
}
.search-wrapper {
width: 400px;
}
}
.dashboard-container {
margin: 22px 0;
width: 100%;
height: calc(100% - 357px);
.header-wrapper {
font-size: var(--font-size-large);
.header {
color: var(--input-text-color);
padding: 6px 8px;
border-radius: #{$border-radius-extra-large};
&.active {
background: var(--background-color-button);
color: var(--text-button-color);
}
}
}
.cards-container {
height: auto;
max-height: 100%;
display: flex; display: flex;
flex-wrap: wrap; flex-direction: column;
position: relative; gap: 16px;
width: 100%; background: var(--background-color);
padding-top: 18px; backdrop-filter: blur(20px);
gap: 18px; border-radius: 30px;
overflow: auto; box-shadow: var(--box-shadow-medium);
}
.side-pannel-header {
@include flex-space-between;
.user-container {
@include flex-center;
gap: 6px;
.user-profile {
height: 32px;
width: 32px;
line-height: 32px;
text-align: center;
font-weight: var(--font-weight-medium);
background: var(--background-color-accent);
color: var(--text-button-color);
border-radius: #{$border-radius-circle};
}
.user-name {
color: var(--accent-color);
}
}
.notifications-container {
@include flex-center;
height: 24px;
width: 24px;
cursor: pointer;
}
}
.new-project-button {
position: relative;
padding: 12px 16px;
color: var(--text-color);
background: #7b4cd323;
border-radius: #{$border-radius-xxx};
overflow: hidden;
cursor: pointer;
transition: color 0.3s;
&::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, -50%);
height: 0px;
width: 0px;
border-radius: 50%;
background: var(--background-color-accent);
z-index: -1;
transition: all 0.25s ease-in-out;
}
&:hover {
color: var(--text-button-color);
&::after {
height: 260px;
width: 260px;
left: 50%;
scale: 1;
}
}
}
.side-bar-content-container {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
.side-bar-options-container {
.option-list {
display: flex;
position: relative;
align-items: center;
gap: 8px;
padding: 8px 10px;
margin: 4px 0;
border-radius: #{$border-radius-extra-large};
width: 100%;
overflow: hidden;
cursor: pointer;
transition: color 0.3s;
&::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, -50%);
height: 0px;
width: 0px;
border-radius: 50%;
background: #7b4cd323;
z-index: -1;
transition: all 0.5s ease-in-out;
}
&:disabled {
cursor: help;
}
&:hover {
&::after {
height: 260px;
width: 260px;
left: 50%;
scale: 1;
}
}
}
.active {
color: var(--text-button-color);
font-weight: var(--font-weight-medium);
background: var(--background-color-button);
&:hover {
background: var(--background-color-button);
}
}
}
}
}
.dashboard-home-container {
width: 100%;
padding-left: 18px;
.dashboard-navbar-container {
margin-top: 28px;
margin-bottom: 22px;
@include flex-center;
.title {
text-transform: capitalize;
font-size: var(--font-size-large);
width: 100%;
}
.market-place-button {
@include flex-center;
gap: 6px;
padding: 8px 14px;
background: var(--background-color-button);
white-space: nowrap;
border-radius: #{$border-radius-extra-large};
color: var(--text-button-color);
}
.search-wrapper {
width: 400px;
}
}
.dashboard-container {
margin: 22px 0;
width: 100%;
height: calc(100% - 357px);
.header-wrapper {
font-size: var(--font-size-large);
.header {
color: var(--input-text-color);
padding: 6px 8px;
border-radius: #{$border-radius-extra-large};
&.active {
background: var(--background-color-button);
color: var(--text-button-color);
}
}
}
.cards-container,
.tutorials-list {
height: auto;
max-height: 100%;
display: flex;
flex-wrap: wrap;
position: relative;
width: 100%;
padding-top: 18px;
gap: 18px;
overflow: auto;
}
}
} }
}
} }
.dashboard-card-container { .dashboard-card-container,
height: 242px; .tutorial-card-container {
width: calc((100% / 5) - 23px); height: 242px;
min-width: 260px; width: calc((100% / 5) - 23px);
position: relative; min-width: 260px;
border: 1px solid var(--border-color);
border-radius: 22px;
cursor: pointer;
overflow: hidden;
&:hover {
border-color: var(--accent-color);
.preview-container {
img {
scale: 1.05;
}
}
}
.dashboard-card-wrapper {
width: 100%;
height: 100%;
position: relative; position: relative;
border: 1px solid var(--border-color);
border-radius: 22px;
cursor: pointer;
overflow: hidden; overflow: hidden;
padding-bottom: 1px; &:hover {
} border-color: var(--accent-color);
.preview-container {
.preview-container { img {
height: 100%; scale: 1.05;
width: 100%; }
border-radius: #{$border-radius-extra-large}; }
overflow: hidden;
img {
height: 100%;
width: 100%;
object-fit: cover;
vertical-align: top;
border: none;
outline: none;
transition: scale 0.2s;
}
}
.project-details-container {
@include flex-space-between;
position: absolute;
bottom: 1px;
width: 100%;
padding: 13px 16px;
background: var(--background-color);
border-radius: #{$border-radius-extra-large};
backdrop-filter: blur(6px);
// transform: translateY(100%);///////hovered
transition: transform 0.2s linear;
.project-details {
display: flex;
flex-direction: column;
align-items: flex-start;
.project-name {
margin-bottom: 7px;
}
.project-data {
text-align: start;
color: var(--input-text-color);
}
} }
.users-list-container { .dashboard-card-wrapper {
@include flex-center; width: 100%;
gap: 6px; height: 100%;
position: relative; // Needed for absolute positioning of kebab-options-wrapper position: relative;
overflow: hidden;
.user-profile { padding-bottom: 1px;
height: 26px;
width: 26px;
line-height: 26px;
text-align: center;
background: var(--background-color-accent);
color: var(--text-button-color);
border-radius: #{$border-radius-circle};
}
.kebab {
padding: 10px;
@include flex-center;
transform: rotate(90deg);
}
} }
}
&:hover { .preview-container {
overflow: visible; height: 100%;
width: 100%;
border-radius: #{$border-radius-extra-large};
overflow: hidden;
.kebab-options-wrapper { img {
display: flex; height: 100%;
width: 100%;
object-fit: cover;
vertical-align: top;
border: none;
outline: none;
transition: scale 0.2s;
}
} }
.project-details-container { .project-details-container {
transform: translateY(0); @include flex-space-between;
position: absolute;
bottom: 1px;
width: 100%;
padding: 13px 16px;
background: var(--background-color);
border-radius: #{$border-radius-extra-large};
backdrop-filter: blur(6px);
// transform: translateY(100%);///////hovered
transition: transform 0.2s linear;
.project-details {
display: flex;
flex-direction: column;
align-items: flex-start;
.project-name {
margin-bottom: 7px;
}
.project-data {
text-align: start;
color: var(--input-text-color);
}
}
.users-list-container {
@include flex-center;
gap: 6px;
position: relative; // Needed for absolute positioning of kebab-options-wrapper
.user-profile {
height: 26px;
width: 26px;
line-height: 26px;
text-align: center;
background: var(--background-color-accent);
color: var(--text-button-color);
border-radius: #{$border-radius-circle};
}
.kebab {
padding: 10px;
@include flex-center;
transform: rotate(90deg);
}
}
}
&:hover {
overflow: visible;
.kebab-options-wrapper {
display: flex;
}
.project-details-container {
transform: translateY(0);
}
} }
}
} }
.market-place-banner-container { .market-place-banner-container {
width: 100%;
height: 230px;
overflow: hidden;
position: relative;
img {
height: 100%;
width: 100%; width: 100%;
object-fit: cover; height: 230px;
border-radius: #{$border-radius-xxx}; overflow: hidden;
} position: relative;
.hero-text { img {
position: absolute; height: 100%;
left: 52px; width: 100%;
bottom: 25px; object-fit: cover;
font-size: 48px; border-radius: #{$border-radius-xxx};
font-family: #{$font-roboto}; }
font-weight: 800;
color: #ffffff;
text-transform: uppercase;
}
.context { .hero-text {
position: absolute; position: absolute;
top: 20px; left: 52px;
right: 58px; bottom: 25px;
text-transform: uppercase; font-size: 48px;
font-size: 22px; font-family: #{$font-roboto};
width: 300px; font-weight: 800;
color: #ffffff; color: #ffffff;
font-family: #{$font-roboto}; text-transform: uppercase;
} }
.arrow-context { .context {
position: absolute; position: absolute;
bottom: 27px; top: 20px;
right: 300px; right: 58px;
} text-transform: uppercase;
font-size: 22px;
width: 300px;
color: #ffffff;
font-family: #{$font-roboto};
}
.explore-button { .arrow-context {
position: absolute; position: absolute;
top: 95px; bottom: 27px;
right: 52px; right: 300px;
padding: 10px 20px; }
text-transform: uppercase;
font-size: 24px; .explore-button {
border: 1px solid #ffffff; position: absolute;
color: #ffffff; top: 95px;
font-family: #{$font-roboto}; right: 52px;
cursor: pointer; padding: 10px 20px;
} text-transform: uppercase;
font-size: 24px;
border: 1px solid #ffffff;
color: #ffffff;
font-family: #{$font-roboto};
cursor: pointer;
}
} }
.kebab-options-wrapper { .kebab-options-wrapper {
min-width: 140px; min-width: 140px;
background: var(--background-color); background: var(--background-color);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
flex-direction: column; flex-direction: column;
// transform: translate(100%, 100%); // transform: translate(100%, 100%);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transform: translate(0%, 0%); transform: translate(0%, 0%);
.option { .option {
padding: 8px 12px; padding: 8px 12px;
font-size: 14px; font-size: 14px;
text-align: left; text-align: left;
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-color); color: var(--text-color);
cursor: pointer; cursor: pointer;
transition: background 0.2s ease; transition: background 0.2s ease;
text-transform: capitalize; text-transform: capitalize;
&:hover { &:hover {
background-color: var(--background-color-selected); background-color: var(--background-color-selected);
}
}
}
// 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);
}
}
}
}
} }
}
} }

View File

@@ -70,3 +70,10 @@ interface ListProps {
items?: ZoneItem[]; items?: ZoneItem[];
remove?: boolean; remove?: boolean;
} }
interface Tutorial {
_id: string;
name: string;
thumbnail?: string;
updatedAt: string;
}