From 9b2c33ffe811feff5cd9f6db214d8227a97d1238 Mon Sep 17 00:00:00 2001 From: Vishnu Date: Mon, 15 Sep 2025 16:05:54 +0530 Subject: [PATCH] feat: Refactor Dashboard components and add Use Cases section - Updated DashboardMain to improve data fetching and caching logic. - Removed unused Modal component and related state management. - Enhanced project deletion and restoration processes with cache updates. - Introduced DashboardUseCases component to display use cases with tutorial cards. - Updated Dashboard to include Use Cases tab and corresponding routing. - Improved styling for dashboard components, including side panel and tutorials list. - Added new Tutorial interface in uiTypes for better type safety. --- .../components/Dashboard/DasboardUseCases.tsx | 75 ++ .../components/Dashboard/DashboardCard.tsx | 89 +- .../components/Dashboard/DashboardMain.tsx | 198 ++--- .../Dashboard/DashboardTutorial.tsx | 20 +- app/src/components/Dashboard/SidePannel.tsx | 44 +- app/src/pages/Dashboard.tsx | 2 + app/src/styles/pages/_dashboard.scss | 830 ++++++++++-------- app/src/types/uiTypes.d.ts | 7 + 8 files changed, 745 insertions(+), 520 deletions(-) create mode 100644 app/src/components/Dashboard/DasboardUseCases.tsx diff --git a/app/src/components/Dashboard/DasboardUseCases.tsx b/app/src/components/Dashboard/DasboardUseCases.tsx new file mode 100644 index 0000000..bc0e247 --- /dev/null +++ b/app/src/components/Dashboard/DasboardUseCases.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState } from "react"; +import DashboardNavBar from "./DashboardNavBar"; +import { projectTutorialApi } from "../../services/dashboard/projectTutorialApi"; +import { TutorialCard } from "./DashboardTutorial"; + +interface Tutorial { + _id: string; + name: string; + thumbnail?: string; + updatedAt: string; +} + +const DashboardUseCases: React.FC = () => { + const [useCases, setUseCases] = useState([ + { + _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(), + }, + ]); + + 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 ( +
+ +
+
+
+
+ +
+
+ {useCases.length > 0 ? ( + useCases.map((tut) => ) + ) : ( +
+ No Use Cases available click on '+' button to add +
+ )} +
+
+
+ ); +}; + +export default DashboardUseCases; diff --git a/app/src/components/Dashboard/DashboardCard.tsx b/app/src/components/Dashboard/DashboardCard.tsx index 75fcdf8..0f75697 100644 --- a/app/src/components/Dashboard/DashboardCard.tsx +++ b/app/src/components/Dashboard/DashboardCard.tsx @@ -7,6 +7,7 @@ import { useLoadingProgress, useProjectName, useSocketStore } from "../../store/ import OuterClick from "../../utils/outerClick"; import { KebabIcon } from "../icons/ExportCommonIcons"; import { getAllProjectsApi } from "../../services/dashboard/getAllProjectsApi"; +import { Modal } from "../templates/PreviewModal"; // import { viewProject } from "../../services/dashboard/viewProject"; // import { updateProject } from "../../services/dashboard/updateProject"; @@ -20,12 +21,19 @@ interface DashBoardCardProps { handleDeleteProject?: (projectId: string) => Promise; handleTrashDeleteProject?: (projectId: string) => Promise; handleRestoreProject?: (projectId: string) => Promise; - handleDuplicateWorkspaceProject?: (projectId: string, projectName: string, thumbnail: string, userId?: string) => Promise; - handleDuplicateRecentProject?: (projectId: string, projectName: string, thumbnail: string) => Promise; + handleDuplicateWorkspaceProject?: ( + projectId: string, + projectName: string, + thumbnail: string, + userId?: string + ) => Promise; + handleDuplicateRecentProject?: ( + projectId: string, + projectName: string, + thumbnail: string + ) => Promise; active?: "shared" | "trash" | "recent" | string; setIsSearchActive?: React.Dispatch>; - setRecentDuplicateData?: React.Dispatch>; - setProjectDuplicateData?: React.Dispatch>; setActiveFolder?: React.Dispatch>; openKebabProjectId: string | null; setOpenKebabProjectId: React.Dispatch>; @@ -51,8 +59,6 @@ const DashboardCard: React.FC = ({ handleDuplicateRecentProject, createdAt, createdBy, - setRecentDuplicateData, - setProjectDuplicateData, setActiveFolder, openKebabProjectId, setOpenKebabProjectId, @@ -67,6 +73,8 @@ const DashboardCard: React.FC = ({ const [renameValue, setRenameValue] = useState(projectName); const [isRenaming, setIsRenaming] = useState(false); const kebabRef = useRef(null); + const [showDelete, setShowDelete] = useState(false); + const [confirmText, setConfirmText] = useState(""); // Close kebab when clicking outside OuterClick({ @@ -91,6 +99,12 @@ const DashboardCard: React.FC = ({ return kebabOptionsMap.default; }, [active, createdBy, userId]); + const handlePermanentDeleteConformation = () => { + handleTrashDeleteProject?.(projectId); + setShowDelete(false); + setConfirmText(""); + }; + const handleProjectName = useCallback( async (newName: string) => { setRenameValue(newName); @@ -98,7 +112,9 @@ const DashboardCard: React.FC = ({ try { const projects = await getAllProjectsApi(); - const projectUuid = projects?.Projects?.find((val: any) => val.projectUuid === projectId || val._id === projectId); + const projectUuid = projects?.Projects?.find( + (val: any) => val.projectUuid === projectId || val._id === projectId + ); if (!projectUuid) return; const updatePayload = { @@ -122,7 +138,9 @@ const DashboardCard: React.FC = ({ 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); @@ -136,18 +154,16 @@ const DashboardCard: React.FC = ({ break; case "duplicate": if (handleDuplicateWorkspaceProject) { - setProjectDuplicateData?.({ projectId, projectName, thumbnail }); - await handleDuplicateWorkspaceProject(projectId, projectName, thumbnail, userId); + 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; @@ -160,13 +176,10 @@ const DashboardCard: React.FC = ({ userId, active, handleDeleteProject, - handleTrashDeleteProject, handleRestoreProject, handleDuplicateWorkspaceProject, handleDuplicateRecentProject, setProjectName, - setProjectDuplicateData, - setRecentDuplicateData, setActiveFolder, ] ); @@ -237,13 +250,16 @@ const DashboardCard: React.FC = ({ )} {createdAt && (
- {active === "trash" ? "Trashed" : "Edited"} {getRelativeTime(createdAt)} + {active === "trash" ? "Trashed" : "Edited"}{" "} + {getRelativeTime(createdAt)}
)}
-
{(createdBy?.userName || userName || "A").charAt(0).toUpperCase()}
+
+ {(createdBy?.userName || userName || "A").charAt(0).toUpperCase()} +
, document.body )} + + 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, + }, + ]} + /> ); }; diff --git a/app/src/components/Dashboard/DashboardMain.tsx b/app/src/components/Dashboard/DashboardMain.tsx index 56717e5..c8787fa 100644 --- a/app/src/components/Dashboard/DashboardMain.tsx +++ b/app/src/components/Dashboard/DashboardMain.tsx @@ -13,7 +13,6 @@ import { trashSearchProjectApi } from "../../services/dashboard/trashSearchProje import { restoreTrashApi } from "../../services/dashboard/restoreTrashApi"; import { generateUniqueId } from "../../functions/generateUniqueId"; import ProjectSocketRes from "./socket/projectSocketRes"; -import { Modal } from "../templates/PreviewModal"; interface DashboardMainProps { activeFolder: Folder; @@ -22,59 +21,63 @@ interface DashboardMainProps { const DashboardMain: React.FC = ({ activeFolder }) => { const [activeSubFolder, setActiveSubFolder] = useState("myProjects"); const [projectsData, setProjectsData] = useState({}); + const [projectsCache, setProjectsCache] = useState>( + {} + ); const [isSearchActive, setIsSearchActive] = useState(false); - const [duplicateData, setDuplicateData] = useState({}); const [openKebabProjectId, setOpenKebabProjectId] = useState(null); - 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(); - // #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 = { + 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); @@ -92,21 +95,43 @@ const DashboardMain: React.FC = ({ activeFolder }) => { setProjectsData(results?.message ? {} : results); }; - // #region Socket Actions - const handleDelete = async (projectId: string): Promise => { - 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 }; + }); + + 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) || [], + }; }); - } - updateStateAfterRemove(projectId); + return newCache; + }); + + setIsSearchActive(false); }; - const handlePermanentDelete = async (): Promise => { - if (!deleteTargetId) return; + const handleDelete = async (projectId: string): Promise => { + 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 => { + console.log("deleteTargetId: ", deleteTargetId); + if (!deleteTargetId) return; if (projectSocket) { projectSocket.emit("v1:trash:delete", { projectId: deleteTargetId, @@ -115,14 +140,27 @@ const DashboardMain: React.FC = ({ 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 => { 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 ( @@ -140,18 +178,11 @@ const DashboardMain: React.FC = ({ 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]; @@ -171,23 +202,19 @@ const DashboardMain: React.FC = ({ 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 => { - setDeleteTargetId(id); - setShowDelete(true); + handlePermanentDelete(id); }, active: "trash", })} @@ -197,12 +224,6 @@ const DashboardMain: React.FC = ({ activeFolder }) => { )); }; - // #region Use Effects - useEffect(() => { - if (!isSearchActive) fetchData(); - // eslint-disable-next-line - }, [activeFolder, isSearchActive, activeSubFolder]); - return (
= ({ activeFolder }) => { : { setWorkspaceProjects: setProjectsData })} />
- - {/* 🔹 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 ce90bc7..822c4d9 100644 --- a/app/src/components/Dashboard/DashboardTutorial.tsx +++ b/app/src/components/Dashboard/DashboardTutorial.tsx @@ -4,18 +4,11 @@ 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 }) => { +export const TutorialCard: React.FC<{ tutorial: Tutorial }> = ({ tutorial }) => { return (
= ({ tutorial }) => { }} >
-
{tutorial.name}
-
- {new Date(tutorial.updatedAt).toLocaleDateString()} +
+
{tutorial.name}
+
+ {new Date(tutorial.updatedAt).toLocaleDateString()} +
@@ -65,7 +60,6 @@ const DashboardTutorial: React.FC = () => {
-
{tutorials.length > 0 ? ( tutorials.map((tut) => ) diff --git a/app/src/components/Dashboard/SidePannel.tsx b/app/src/components/Dashboard/SidePannel.tsx index af495ab..3789e93 100644 --- a/app/src/components/Dashboard/SidePannel.tsx +++ b/app/src/components/Dashboard/SidePannel.tsx @@ -72,7 +72,11 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => {
{userName?.charAt(0).toUpperCase()}
-
{userName ? userName.charAt(0).toUpperCase() + userName.slice(1).toLowerCase() : "Anonymous"}
+
+ {userName + ? userName.charAt(0).toUpperCase() + userName.slice(1).toLowerCase() + : "Anonymous"} +
@@ -84,34 +88,40 @@ const SidePannel: React.FC = ({ setActiveTab, activeTab }) => {
+
diff --git a/app/src/pages/Dashboard.tsx b/app/src/pages/Dashboard.tsx index 6e6a2a1..de1bee9 100644 --- a/app/src/pages/Dashboard.tsx +++ b/app/src/pages/Dashboard.tsx @@ -4,6 +4,7 @@ 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"; const Dashboard: React.FC = () => { const [activeTab, setActiveTab] = useState("Home"); @@ -25,6 +26,7 @@ const Dashboard: React.FC = () => { )} {activeTab === "Tutorials" && } + {activeTab === "Use Cases" && }
); }; diff --git a/app/src/styles/pages/_dashboard.scss b/app/src/styles/pages/_dashboard.scss index eb98525..5ecea08 100644 --- a/app/src/styles/pages/_dashboard.scss +++ b/app/src/styles/pages/_dashboard.scss @@ -2,401 +2,497 @@ @use "../abstracts/mixins.scss" as *; .dashboard-main { - height: 100vh; - width: 100vw; - display: flex; - padding: 27px 17px; - - .side-pannel-container { - padding: 32px; - min-width: 280px; - height: 100%; + height: 100vh; + width: 100vw; display: flex; - flex-direction: column; - gap: 16px; - background: var(--background-color); - backdrop-filter: blur(20px); - border-radius: 30px; - box-shadow: var(--box-shadow-medium); + padding: 27px 17px; - .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 { - height: auto; - max-height: 100%; + .side-pannel-container { + padding: 32px; + min-width: 280px; + height: 100%; display: flex; - flex-wrap: wrap; - position: relative; - width: 100%; - padding-top: 18px; - gap: 18px; - overflow: auto; - } + flex-direction: column; + gap: 16px; + background: var(--background-color); + backdrop-filter: blur(20px); + border-radius: 30px; + 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 { - height: 242px; - width: calc((100% / 5) - 23px); - min-width: 260px; - position: relative; - 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%; +.dashboard-card-container, +.tutorial-card-container { + height: 242px; + width: calc((100% / 5) - 23px); + min-width: 260px; position: relative; + border: 1px solid var(--border-color); + border-radius: 22px; + cursor: pointer; overflow: hidden; - padding-bottom: 1px; - } - - .preview-container { - height: 100%; - 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); - } + &:hover { + border-color: var(--accent-color); + .preview-container { + img { + scale: 1.05; + } + } } - .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); - } + .dashboard-card-wrapper { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; + padding-bottom: 1px; } - } - &:hover { - overflow: visible; + .preview-container { + height: 100%; + width: 100%; + border-radius: #{$border-radius-extra-large}; + overflow: hidden; - .kebab-options-wrapper { - display: flex; + img { + height: 100%; + width: 100%; + object-fit: cover; + vertical-align: top; + border: none; + outline: none; + transition: scale 0.2s; + } } .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 { - width: 100%; - height: 230px; - overflow: hidden; - position: relative; - - img { - height: 100%; width: 100%; - object-fit: cover; - border-radius: #{$border-radius-xxx}; - } + height: 230px; + overflow: hidden; + position: relative; - .hero-text { - position: absolute; - left: 52px; - bottom: 25px; - font-size: 48px; - font-family: #{$font-roboto}; - font-weight: 800; - color: #ffffff; - text-transform: uppercase; - } + img { + height: 100%; + width: 100%; + object-fit: cover; + border-radius: #{$border-radius-xxx}; + } - .context { - position: absolute; - top: 20px; - right: 58px; - text-transform: uppercase; - font-size: 22px; - width: 300px; - color: #ffffff; - font-family: #{$font-roboto}; - } + .hero-text { + position: absolute; + left: 52px; + bottom: 25px; + font-size: 48px; + font-family: #{$font-roboto}; + font-weight: 800; + color: #ffffff; + text-transform: uppercase; + } - .arrow-context { - position: absolute; - bottom: 27px; - right: 300px; - } + .context { + position: absolute; + top: 20px; + right: 58px; + text-transform: uppercase; + font-size: 22px; + width: 300px; + color: #ffffff; + font-family: #{$font-roboto}; + } - .explore-button { - position: absolute; - top: 95px; - right: 52px; - padding: 10px 20px; - text-transform: uppercase; - font-size: 24px; - border: 1px solid #ffffff; - color: #ffffff; - font-family: #{$font-roboto}; - cursor: pointer; - } + .arrow-context { + position: absolute; + bottom: 27px; + right: 300px; + } + + .explore-button { + position: absolute; + top: 95px; + right: 52px; + padding: 10px 20px; + text-transform: uppercase; + font-size: 24px; + border: 1px solid #ffffff; + color: #ffffff; + font-family: #{$font-roboto}; + cursor: pointer; + } } .kebab-options-wrapper { - min-width: 140px; - background: var(--background-color); - border: 1px solid var(--border-color); - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - backdrop-filter: blur(20px); - flex-direction: column; - // transform: translate(100%, 100%); - overflow: hidden; - display: flex; - flex-direction: column; - transform: translate(0%, 0%); - .option { - padding: 8px 12px; - font-size: 14px; - text-align: left; - background: transparent; - border: none; - color: var(--text-color); - cursor: pointer; - transition: background 0.2s ease; - text-transform: capitalize; + min-width: 140px; + background: var(--background-color); + border: 1px solid var(--border-color); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(20px); + flex-direction: column; + // transform: translate(100%, 100%); + overflow: hidden; + display: flex; + flex-direction: column; + transform: translate(0%, 0%); + .option { + padding: 8px 12px; + font-size: 14px; + text-align: left; + background: transparent; + border: none; + color: var(--text-color); + cursor: pointer; + transition: background 0.2s ease; + text-transform: capitalize; - &:hover { - background-color: var(--background-color-selected); + &:hover { + 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); + } + } + } + } } - } } diff --git a/app/src/types/uiTypes.d.ts b/app/src/types/uiTypes.d.ts index d6b1c79..89b0eba 100644 --- a/app/src/types/uiTypes.d.ts +++ b/app/src/types/uiTypes.d.ts @@ -70,3 +70,10 @@ interface ListProps { items?: ZoneItem[]; remove?: boolean; } + +interface Tutorial { + _id: string; + name: string; + thumbnail?: string; + updatedAt: string; +} \ No newline at end of file