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.
This commit is contained in:
2025-09-15 16:05:54 +05:30
parent 0f478e8e1a
commit 9b2c33ffe8
8 changed files with 745 additions and 520 deletions

View File

@@ -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<Tutorial[]>([
{
_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 (
<div className="dashboard-home-container">
<DashboardNavBar page="tutorial" />
<div className="dashboard-container" style={{ height: "calc(100% - 87px)" }}>
<div className="tutorials-list">
<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

@@ -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<void>;
handleTrashDeleteProject?: (projectId: string) => Promise<void>;
handleRestoreProject?: (projectId: string) => Promise<void>;
handleDuplicateWorkspaceProject?: (projectId: string, projectName: string, thumbnail: string, userId?: string) => Promise<void>;
handleDuplicateRecentProject?: (projectId: string, projectName: string, thumbnail: string) => Promise<void>;
handleDuplicateWorkspaceProject?: (
projectId: string,
projectName: string,
thumbnail: string,
userId?: string
) => Promise<void>;
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>>;
@@ -51,8 +59,6 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
handleDuplicateRecentProject,
createdAt,
createdBy,
setRecentDuplicateData,
setProjectDuplicateData,
setActiveFolder,
openKebabProjectId,
setOpenKebabProjectId,
@@ -67,6 +73,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({
@@ -91,6 +99,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);
@@ -98,7 +112,9 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
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<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);
@@ -136,18 +154,16 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
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<DashBoardCardProps> = ({
userId,
active,
handleDeleteProject,
handleTrashDeleteProject,
handleRestoreProject,
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
setProjectName,
setProjectDuplicateData,
setRecentDuplicateData,
setActiveFolder,
]
);
@@ -237,13 +250,16 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
)}
{createdAt && (
<div className="project-data">
{active === "trash" ? "Trashed" : "Edited"} {getRelativeTime(createdAt)}
{active === "trash" ? "Trashed" : "Edited"}{" "}
{getRelativeTime(createdAt)}
</div>
)}
</div>
<div className="users-list-container" ref={kebabRef}>
<div className="user-profile">{(createdBy?.userName || userName || "A").charAt(0).toUpperCase()}</div>
<div className="user-profile">
{(createdBy?.userName || userName || "A").charAt(0).toUpperCase()}
</div>
<button
className="kebab-wrapper"
onClick={(e) => {
@@ -262,7 +278,10 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
{isKebabOpen &&
createPortal(
<div className={`kebab-options-wrapper tag-${projectId}`} style={{ position: "fixed", zIndex: 9999, ...kebabPosition }}>
<div
className={`kebab-options-wrapper tag-${projectId}`}
style={{ position: "fixed", zIndex: 9999, ...kebabPosition }}
>
{getOptions().map((option) => (
<button
key={option}
@@ -279,6 +298,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>
);
};

View File

@@ -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<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);
@@ -92,21 +95,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 };
});
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<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,
@@ -115,14 +140,27 @@ 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 (
@@ -140,18 +178,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];
@@ -171,23 +202,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",
})}
@@ -197,12 +224,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
@@ -244,33 +265,6 @@ const DashboardMain: React.FC<DashboardMainProps> = ({ activeFolder }) => {
: { 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>
);
};

View File

@@ -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 (
<div className="tutorial-card-container">
<div
className="thumbnail"
className="preview-container"
style={{
backgroundImage: tutorial.thumbnail
? `url(${tutorial.thumbnail})`
@@ -23,9 +16,11 @@ const TutorialCard: React.FC<{ tutorial: Tutorial }> = ({ tutorial }) => {
}}
></div>
<div className="tutorial-details">
<div className="tutorial-name">{tutorial.name}</div>
<div className="updated-date">
{new Date(tutorial.updatedAt).toLocaleDateString()}
<div className="context">
<div className="tutorial-name">{tutorial.name}</div>
<div className="updated-date">
{new Date(tutorial.updatedAt).toLocaleDateString()}
</div>
</div>
<div className="delete-option">
<DeleteIcon />
@@ -65,7 +60,6 @@ const DashboardTutorial: React.FC = () => {
</button>
</div>
</div>
<div className="tutorials-list">
{tutorials.length > 0 ? (
tutorials.map((tut) => <TutorialCard key={tut._id} tutorial={tut} />)

View File

@@ -72,7 +72,11 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
<div className="side-pannel-header">
<div className="user-container">
<div className="user-profile">{userName?.charAt(0).toUpperCase()}</div>
<div className="user-name">{userName ? userName.charAt(0).toUpperCase() + userName.slice(1).toLowerCase() : "Anonymous"}</div>
<div className="user-name">
{userName
? userName.charAt(0).toUpperCase() + userName.slice(1).toLowerCase()
: "Anonymous"}
</div>
</div>
<div className="notifications-container">
<NotificationIcon />
@@ -84,34 +88,40 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
<div className="side-bar-content-container">
<div className="side-bar-options-container">
<button
className={
activeTab === "Home" ? "option-list active" : "option-list"
}
className={activeTab === "Home" ? "option-list active" : "option-list"}
onClick={() => setActiveTab("Home")}
>
<HomeIcon isActive={activeTab === 'Home'}/>
<HomeIcon isActive={activeTab === "Home"} />
Home
</button>
<button
className={
activeTab === "Projects" ? "option-list active" : "option-list"
}
className={activeTab === "Projects" ? "option-list active" : "option-list"}
title="Projects"
onClick={() => setActiveTab("Projects")}
>
<ProjectsIcon isActive={activeTab === 'Projects'}/>
<ProjectsIcon isActive={activeTab === "Projects"} />
Projects
</button>
<button
className={
activeTab === "Trash" ? "option-list active" : "option-list"
}
className={activeTab === "Trash" ? "option-list active" : "option-list"}
title="Trash"
onClick={() => setActiveTab("Trash")}
>
<TrashIcon isActive={activeTab === 'Trash'}/>
<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"
@@ -121,11 +131,13 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
console.warn("Tutorials comming soon");
}}
>
<TutorialsIcon isActive={activeTab === 'Tutorials'}/>
<TutorialsIcon isActive={activeTab === "Tutorials"} />
Tutorials
</button>
<button
className={activeTab === "Documentation" ? "option-list active" : "option-list"}
className={
activeTab === "Documentation" ? "option-list active" : "option-list"
}
title="coming soon"
disabled // remove when added
onClick={() => {
@@ -133,7 +145,7 @@ const SidePannel: React.FC<SidePannelProps> = ({ setActiveTab, activeTab }) => {
console.warn("Documentation comming soon");
}}
>
<DocumentationIcon isActive={activeTab === 'Documentation'}/>
<DocumentationIcon isActive={activeTab === "Documentation"} />
Documentation
</button>
</div>

View File

@@ -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<string>("Home");
@@ -25,6 +26,7 @@ const Dashboard: React.FC = () => {
<DashboardMain activeFolder={activeTab.toLowerCase() as Folder} />
)}
{activeTab === "Tutorials" && <DashboardTutorial />}
{activeTab === "Use Cases" && <DashboardUseCases />}
</div>
);
};

View File

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

View File

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