v2-ui #94

Merged
Vishnu merged 38 commits from v2-ui into main 2025-05-26 05:09:47 +00:00
43 changed files with 1943 additions and 758 deletions

View File

@ -1058,9 +1058,6 @@ export const SaveIcon = () => {
);
};
export const SaveVersionIcon = () => {
return (
<svg
@ -1070,10 +1067,12 @@ export const SaveVersionIcon = () => {
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<foreignObject x="-38.7816" y="-60.596" width="261.775" height="235.114">
</foreignObject>
<foreignObject
x="-38.7816"
y="-60.596"
width="261.775"
height="235.114"
></foreignObject>
<rect
data-figma-bg-blur-radius="60.596"
x="22.4204"
@ -1226,3 +1225,57 @@ export const SaveVersionIcon = () => {
</svg>
);
};
export const RenameVersionIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.7995 4.19826L18.9416 7.44492C19.8527 7.92654 19.8527 9.23161 18.9416 9.71322L17.8513 10.2896L18.9416 10.8658C19.8527 11.3475 19.8527 12.6526 18.9416 13.1342L17.8513 13.7105L18.9416 14.2868C19.8527 14.7684 19.8527 16.0735 18.9416 16.5551L12.7995 19.8017C12.2995 20.0661 11.701 20.0661 11.2008 19.8017L5.05883 16.5551C4.1477 16.0735 4.14771 14.7684 5.05883 14.2868L6.14911 13.7105L5.05883 13.1342C4.1477 12.6526 4.14771 11.3475 5.05883 10.8658L6.14911 10.2896L5.05883 9.71322C4.14771 9.2316 4.14771 7.92654 5.05883 7.44492L11.2008 4.19826C11.701 3.93391 12.2995 3.93391 12.7995 4.19826ZM16.0212 14.6779L12.7995 16.3808C12.2995 16.6452 11.701 16.6452 11.2008 16.3808L7.97918 14.6779L6.57338 15.421L12.0002 18.2895L17.427 15.421L16.0212 14.6779ZM16.0212 11.2569L12.7995 12.9599C12.3449 13.2002 11.809 13.222 11.3395 13.0254L11.2008 12.9599L7.97918 11.2569L6.57338 12L12.0002 14.8686L17.427 12L16.0212 11.2569ZM12.0002 5.71047L6.57338 8.57907L12.0002 11.4476L17.427 8.57907L12.0002 5.71047Z"
fill="var(--text-color)"
/>
</svg>
);
};
export const FinishEditIcon = () => {
return (
<svg
width="61"
height="60"
viewBox="0 0 61 60"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M30.5 52C18.3688 52 8.5 42.1307 8.5 30C8.5 17.8693 18.3688 8 30.5 8C42.6312 8 52.5 17.8693 52.5 30C52.5 42.1307 42.6312 52 30.5 52Z"
fill="#D7EBFF"
/>
<path
d="M52.5 30C52.5 17.8693 42.6312 8 30.5 8V52C42.6312 52 52.5 42.1307 52.5 30Z"
fill="#C4E2FF"
/>
<path
d="M30.4996 49.1281C19.9508 49.1281 11.3691 40.5461 11.3691 29.9977C11.3691 19.4493 19.9508 10.8672 30.4996 10.8672C41.0484 10.8672 49.6301 19.4493 49.6301 29.9977C49.6301 40.546 41.0484 49.1281 30.4996 49.1281Z"
fill="#88CC2A"
/>
<path
d="M49.6305 29.9977C49.6305 19.4493 41.0488 10.8672 30.5 10.8672V49.128C41.0488 49.1281 49.6305 40.546 49.6305 29.9977Z"
fill="#7FB335"
/>
<path
d="M28.5872 38.605C27.8529 38.605 27.1187 38.3247 26.5583 37.7642L20.6454 31.8514C19.5244 30.7309 19.5244 28.9141 20.6454 27.7936C21.7664 26.6726 23.5822 26.6726 24.7032 27.7936L28.5872 31.6771L36.7755 23.4892C37.8964 22.3682 39.7123 22.3682 40.8333 23.4892C41.9542 24.6096 41.9542 26.4264 40.8333 27.547L30.6161 37.7642C30.0557 38.3247 29.3214 38.605 28.5872 38.605Z"
fill="white"
/>
<path
d="M36.7752 23.497L30.5 29.7719V37.8665C30.5374 37.8327 30.5797 37.808 30.6158 37.7719L40.833 27.5547C41.954 26.4342 41.954 24.6174 40.833 23.4969C39.7122 22.376 37.8962 22.376 36.7752 23.497Z"
fill="#EDF0F2"
/>
</svg>
);
};

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
import ToggleHeader from "../../ui/inputs/ToggleHeader";
import Outline from "./Outline";
import Header from "./Header";
import {useToggleStore} from "../../../store/useUIToggleStore";
import { useToggleStore } from "../../../store/useUIToggleStore";
import Assets from "./Assets";
import useModuleStore from "../../../store/useModuleStore";
import Widgets from "./visualization/widgets/Widgets";
@ -36,9 +36,7 @@ const SideBarLeft: React.FC = () => {
return (
<div
className={`sidebar-left-wrapper ${
(toggleUILeft && !isVersionSaved) || activeModule !== "simulation"
? "open"
: "closed"
toggleUILeft && !isVersionSaved ? "open" : "closed"
}`}
>
<Header />

View File

@ -4,8 +4,8 @@ import { useActiveUsers, useCamMode } from "../../../store/builder/store";
import { ActiveUser } from "../../../types/users";
import CollaborationPopup from "../../templates/CollaborationPopup";
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
import { useSelectedUserStore } from "../../../store/useCollabStore";
import {useToggleStore} from "../../../store/useUIToggleStore";
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
import { useToggleStore } from "../../../store/useUIToggleStore";
import { ToggleSidebarIcon } from "../../icons/HeaderIcons";
import useModuleStore from "../../../store/useModuleStore";
@ -62,9 +62,8 @@ const Header: React.FC = () => {
<div className="options-container">
<button
id="toggle-rightSidebar-ui-button"
className={`toggle-sidebar-ui-button ${
!toggleUIRight ? "active" : ""
}`}
className={`toggle-sidebar-ui-button ${!toggleUIRight ? "active" : ""
}`}
onClick={() => {
if (activeModule !== "market") {
setToggleUI(toggleUILeft, !toggleUIRight);

View File

@ -9,7 +9,7 @@ import {
PropertiesIcon,
SimulationIcon,
} from "../../icons/SimulationIcons";
import {useToggleStore} from "../../../store/useUIToggleStore";
import { useToggleStore } from "../../../store/useUIToggleStore";
import Visualization from "./visualization/Visualization";
import Analysis from "./analysis/Analysis";
import Simulations from "./simulation/Simulations";
@ -63,9 +63,7 @@ const SideBarRight: React.FC = () => {
return (
<div
className={`sidebar-right-wrapper ${
(toggleUIRight && !isVersionSaved) || activeModule !== "simulation"
? "open"
: "closed"
toggleUIRight && !isVersionSaved ? "open" : "closed"
}`}
>
<Header />

View File

@ -8,18 +8,19 @@ import {
} from "../../../icons/ExportCommonIcons";
import RenameInput from "../../../ui/inputs/RenameInput";
import { useVersionStore } from "../../../../store/builder/store";
import { generateUniqueId } from "../../../../functions/generateUniqueId";
const VersionHistory = () => {
const userName = localStorage.getItem("userName") ?? "Anonymous";
const { versions, addVersion, setVersions } = useVersionStore();
const { versions, addVersion, setVersions, updateVersion } =
useVersionStore();
const [selectedVersion, setSelectedVersion] = useState(
versions.length > 0 ? versions[0] : null
);
const addNewVersion = () => {
const newVersion = {
id: crypto.randomUUID(),
id: generateUniqueId(),
versionLabel: `v${versions.length + 1}.0`,
versionName: "",
timestamp: new Date().toLocaleDateString("en-US", {
@ -33,23 +34,21 @@ const VersionHistory = () => {
const newVersions = [newVersion, ...versions];
addVersion(newVersion);
setSelectedVersion(newVersion);
setVersions(newVersions); // bring new one to top
setVersions(newVersions);
};
const handleSelectVersion = (version: any) => {
setSelectedVersion(version);
// Move selected version to top, keep others in same order
const reordered = [version, ...versions.filter((v) => v.id !== version.id)];
setVersions(reordered);
};
const handleTimestampChange = (newTimestamp: string, index: number) => {
const updated = [...versions];
updated[index].timestamp = newTimestamp;
console.warn("Timestamp updated locally but not persisted in store.");
setVersions(updated); // Optional: persist timestamp change
const handleVersionNameChange = (newName: string, versionId: string) => {
const updated = versions.map((v) =>
v.id === versionId ? { ...v, versionName: newName } : v
);
setVersions(updated);
updateVersion(versionId, { versionName: newName });
};
return (
@ -99,28 +98,36 @@ const VersionHistory = () => {
</div>
)}
{/* Versions List or No Versions Message */}
{/* Versions List */}
<div className="saved-versions-list">
{versions.length === 0 ? (
<div className="no-versions-message">No saved versions</div>
) : (
versions.map((version, index) => (
versions.map((version) => (
<button
key={version.id}
className="saved-version"
id={`${version.versionName}-${index}`}
onClick={() => handleSelectVersion(version)}
>
<div className="version-name">{version.versionLabel}</div>
<div className="version-details">
<div className="details">
<span className="timestamp">
<RenameInput
value={version.timestamp}
onRename={(newTimestamp) =>
handleTimestampChange(newTimestamp, index)
}
/>
{version.versionName ? (
<RenameInput
value={version.versionName}
onRename={(newName) =>
handleVersionNameChange(newName, version.id)
}
/>
) : (
<RenameInput
value={version.timestamp}
onRename={(newName) =>
handleVersionNameChange(newName, version.id)
}
/>
)}
</span>
<span className="saved-by">
<div className="user-profile">{version.savedBy[0]}</div>

View File

@ -2,10 +2,11 @@ import React, { useState, useEffect, useRef } from "react";
import { useVersionStore } from "../../../../store/builder/store";
import {
CloseIcon,
FinishEditIcon,
RenameVersionIcon,
SaveIcon,
SaveVersionIcon,
} from "../../../icons/ExportCommonIcons";
import { RenameIcon } from "../../../icons/ContextMenuIcons";
import RenderOverlay from "../../../templates/Overlay";
const VersionSaved = () => {
@ -15,28 +16,28 @@ const VersionSaved = () => {
const [showNotification, setShowNotification] = useState(false);
const [newName, setNewName] = useState("");
const [description, setDescription] = useState("");
const [showEditedFinish, setShowEditedFinish] = useState(false);
const [editedVersionName, setEditedVersionName] = useState("");
const prevVersionCount = useRef(versions.length);
const dismissTimerRef = useRef<NodeJS.Timeout | null>(null);
const latestVersion = versions?.[0];
// Clear dismiss timer when component unmounts
useEffect(() => {
return () => {
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
};
}, []);
// Handle new version notification and setup dismiss timer
useEffect(() => {
if (versions.length > prevVersionCount.current) {
if (versions.length > prevVersionCount.current && latestVersion) {
setShowNotification(true);
setShouldDismiss(false);
setIsEditing(false);
setNewName(versions[0].versionName ?? "");
setDescription(versions[0]?.description ?? "");
setNewName(latestVersion.versionName ?? "");
setDescription(latestVersion.description ?? "");
setEditedVersionName(latestVersion.versionName ?? ""); // Initialize editedVersionName
// Only start dismiss timer if not in edit mode
if (!isEditing) {
startDismissTimer();
}
@ -45,18 +46,15 @@ const VersionSaved = () => {
} else if (versions.length < prevVersionCount.current) {
prevVersionCount.current = versions.length;
}
}, [versions, isEditing]);
}, [versions, isEditing, latestVersion]);
// Start or restart the dismiss timer
const startDismissTimer = (delay = 5000) => {
if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
dismissTimerRef.current = setTimeout(() => {
console.log("isEditing: ", isEditing);
setShouldDismiss(true);
}, delay);
};
// Hide notification after dismiss animation delay
useEffect(() => {
if (shouldDismiss) {
const timer = setTimeout(() => setShowNotification(false), 200);
@ -65,11 +63,11 @@ const VersionSaved = () => {
}, [shouldDismiss]);
const handleEditName = () => {
setIsEditing(true);
setNewName(latestVersion?.versionName ?? "");
setDescription(latestVersion?.description ?? "");
if (!latestVersion) return;
// Clear any existing dismiss timer when editing starts
setIsEditing(true);
setNewName(latestVersion.versionName ?? "");
setDescription(latestVersion.description ?? "");
if (dismissTimerRef.current) {
clearTimeout(dismissTimerRef.current);
dismissTimerRef.current = null;
@ -77,21 +75,29 @@ const VersionSaved = () => {
};
const handleFinishEdit = () => {
if (latestVersion) {
updateVersion(latestVersion.id, {
versionName: newName,
description,
});
console.log("saved");
startDismissTimer(); // Restart 5s timer after save
}
startDismissTimer(); // Restart 5s timer after save
if (!latestVersion) return;
const updatedName =
(newName.trim() || latestVersion.versionName) ?? latestVersion.timestamp;
updateVersion(latestVersion.id, {
versionName: updatedName,
description,
});
setEditedVersionName(updatedName);
setIsEditing(false);
setShowEditedFinish(true);
setTimeout(() => {
setShowEditedFinish(false);
}, 5000);
startDismissTimer();
};
const handleCancel = () => {
setIsEditing(false);
startDismissTimer(); // Restart 5s timer after cancel
startDismissTimer();
};
const handleClose = () => {
@ -103,35 +109,39 @@ const VersionSaved = () => {
return (
<div className={`versionSaved ${shouldDismiss ? "dismissing" : ""}`}>
<div className="version-header">
<div className="header-wrapper">
<div className="icon">
<SaveIcon />
{!isEditing && !showEditedFinish && (
<div className="versionSaved-wrapper">
<div className="version-header">
<div className="header-wrapper">
<div className="icon">
<SaveIcon />
</div>
<span>Saved New Version</span>
</div>
<button className="close-btn" onClick={handleClose}>
<CloseIcon />
</button>
</div>
<span>Saved New Version</span>
</div>
<button className="close-btn" onClick={handleClose}>
<CloseIcon />
</button>
</div>
<div className="version-details">
<SaveVersionIcon />
<div className="details">
<div className="details-wrapper">
New Version Created {latestVersion.versionLabel}{" "}
{latestVersion.timestamp.toUpperCase()}
<div className="version-details">
<SaveVersionIcon />
<div className="details">
<div className="details-wrapper">
New Version Created {latestVersion.versionLabel}{" "}
{latestVersion.timestamp.toUpperCase()}
</div>
<button onClick={handleEditName}>Edit name</button>
</div>
</div>
<button onClick={handleEditName}>Edit name</button>
</div>
</div>
)}
{isEditing && (
<RenderOverlay>
<div className="edit-version-popup-wrapper">
<div className="details-wrapper-popup-container">
<div className="header-wrapper">
<RenameIcon />
<RenameVersionIcon />
<div className="label">Rename Version</div>
</div>
<div className="details-wrapper">
@ -154,16 +164,11 @@ const VersionSaved = () => {
</div>
</div>
<div className="version-description">
{/* <input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add description"
/> */}
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add description"
style={{ resize: "none" }}
/>
</div>
</div>
@ -179,6 +184,22 @@ const VersionSaved = () => {
</div>
</RenderOverlay>
)}
{showEditedFinish && (
<RenderOverlay>
<div className="finishEdit-version-popup-wrapper">
<div className="finishEdit-wrapper-popup-container">
<div className="icon">
<FinishEditIcon />
</div>
<div className="versionname">
{editedVersionName || latestVersion.versionName}
</div>
<div className="success-message">Saved Successfully!</div>
</div>
</div>
</RenderOverlay>
)}
</div>
);
};

View File

@ -12,140 +12,198 @@ import Widget4InputCard3D from './Widget4InputCard3D'
import WarehouseThroughputInputComponent from './WarehouseThroughputInputComponent'
import { useWidgetStore } from '../../../../../store/useWidgetStore'
// const InputSelecterComponent = () => {
// const { selectedChartId } = useWidgetStore();
// if (selectedChartId && selectedChartId.type && selectedChartId.type === 'bar' ) {
// return (
// <>
// <div className="sideBarHeader">2D Widget Input</div>
// <BarChartInput />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'line' ) {
// return (
// <>
// <div className="sideBarHeader">2D Widget Input</div>
// <LineGrapInput />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'pie' ) {
// return (
// <>
// <div className="sideBarHeader">2D Widget Input</div>
// <PieChartInput />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'doughnut' ) {
// return (
// <>
// <div className="sideBarHeader">2D Widget Input</div>
// <PieChartInput />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'polarArea' ) {
// return (
// <>
// <div className="sideBarHeader">2D Widget Input</div>
// <PieChartInput />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'progress 1' ) {
// return (
// <>
// <div className="sideBarHeader">2D Widget Input</div>
// <Progress1Input />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'progress 2' ) {
// return (
// <>
// <div className="sideBarHeader">2D Widget Input</div>
// <Progress2Input />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.className && selectedChartId.className === 'warehouseThroughput floating' ) {
// return (
// <>
// <div className="sideBarHeader">Floting Widget Input</div>
// <WarehouseThroughputInputComponent />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.className && selectedChartId.className === 'fleetEfficiency floating' ) {
// return (
// <>
// <div className="sideBarHeader">Floting Widget Input</div>
// <FleetEfficiencyInputComponent />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.className && selectedChartId.className === 'floating total-card' ) {
// return (
// <>
// <div className="sideBarHeader">Floting Widget Input</div>
// <FleetEfficiencyInputComponent />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'ui-Widget 1' ) {
// return (
// <>
// <div className="sideBarHeader">3D Widget Input</div>
// <Widget4InputCard3D />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'ui-Widget 2' ) {
// return (
// <>
// <div className="sideBarHeader">3D Widget Input</div>
// <Widget2InputCard3D />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'ui-Widget 3' ) {
// return (
// <>
// <div className="sideBarHeader">3D Widget Input</div>
// <Widget3InputCard3D />
// </>
// )
// }
// else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'ui-Widget 4' ) {
// return (
// <>
// <div className="sideBarHeader">3D Widget Input</div>
// <Widget4InputCard3D />
// </>
// )
// }
// else {
// return (
// <div>No chart selected</div>
// )
// }
// }
const chartTypeMap: Record<| 'bar'| 'line'| 'pie' | 'doughnut' | 'polarArea'| 'progress 1' | 'progress 2'
| 'ui-Widget 1'| 'ui-Widget 2'| 'ui-Widget 3'| 'ui-Widget 4',JSX.Element> = {
bar: <BarChartInput />,
line: <LineGrapInput />,
pie: <PieChartInput />,
doughnut: <PieChartInput />,
polarArea: <PieChartInput />,
'progress 1': <Progress1Input />,
'progress 2': <Progress2Input />,
'ui-Widget 1': <Widget4InputCard3D />,
'ui-Widget 2': <Widget2InputCard3D />,
'ui-Widget 3': <Widget3InputCard3D />,
'ui-Widget 4': <Widget4InputCard3D />,
};
const classNameMap: Record<
| 'warehouseThroughput floating'
| 'fleetEfficiency floating'
| 'floating total-card',
JSX.Element
> = {
'warehouseThroughput floating': <WarehouseThroughputInputComponent />,
'fleetEfficiency floating': <FleetEfficiencyInputComponent />,
'floating total-card': <FleetEfficiencyInputComponent />,
};
const InputSelecterComponent = () => {
const { selectedChartId } = useWidgetStore();
const { selectedChartId } = useWidgetStore();
if (selectedChartId && selectedChartId.type && selectedChartId.type === 'bar' ) {
return (
if (selectedChartId) {
const { type, className } = selectedChartId;
if (type && chartTypeMap[type as keyof typeof chartTypeMap]) {
const label = ['ui-Widget 1', 'ui-Widget 2', 'ui-Widget 3', 'ui-Widget 4'].includes(type)
? '3D Widget Input'
: '2D Widget Input';
return (
<>
<div className="sideBarHeader">2D Widget Input</div>
<BarChartInput />
<div className="sideBarHeader">{label}</div>
{chartTypeMap[type as keyof typeof chartTypeMap]}
</>
)
}
);
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'line' ) {
return (
if (className && classNameMap[className as keyof typeof classNameMap]) {
return (
<>
<div className="sideBarHeader">2D Widget Input</div>
<LineGrapInput />
<div className="sideBarHeader">Floting Widget Input</div>
{classNameMap[className as keyof typeof classNameMap]}
</>
)
);
}
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'pie' ) {
return (
<>
<div className="sideBarHeader">2D Widget Input</div>
<PieChartInput />
</>
)
}
return <div>No chart selected</div>;
};
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'doughnut' ) {
return (
<>
<div className="sideBarHeader">2D Widget Input</div>
<PieChartInput />
</>
)
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'polarArea' ) {
return (
<>
<div className="sideBarHeader">2D Widget Input</div>
<PieChartInput />
</>
)
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'progress 1' ) {
return (
<>
<div className="sideBarHeader">2D Widget Input</div>
<Progress1Input />
</>
)
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'progress 2' ) {
return (
<>
<div className="sideBarHeader">2D Widget Input</div>
<Progress2Input />
</>
)
}
else if (selectedChartId && selectedChartId.className && selectedChartId.className === 'warehouseThroughput floating' ) {
return (
<>
<div className="sideBarHeader">Floting Widget Input</div>
<WarehouseThroughputInputComponent />
</>
)
}
else if (selectedChartId && selectedChartId.className && selectedChartId.className === 'fleetEfficiency floating' ) {
return (
<>
<div className="sideBarHeader">Floting Widget Input</div>
<FleetEfficiencyInputComponent />
</>
)
}
else if (selectedChartId && selectedChartId.className && selectedChartId.className === 'floating total-card' ) {
return (
<>
<div className="sideBarHeader">Floting Widget Input</div>
<FleetEfficiencyInputComponent />
</>
)
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'ui-Widget 1' ) {
return (
<>
<div className="sideBarHeader">3D Widget Input</div>
<Widget4InputCard3D />
</>
)
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'ui-Widget 2' ) {
return (
<>
<div className="sideBarHeader">3D Widget Input</div>
<Widget2InputCard3D />
</>
)
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'ui-Widget 3' ) {
return (
<>
<div className="sideBarHeader">3D Widget Input</div>
<Widget3InputCard3D />
</>
)
}
else if (selectedChartId && selectedChartId.type && selectedChartId.type === 'ui-Widget 4' ) {
return (
<>
<div className="sideBarHeader">3D Widget Input</div>
<Widget4InputCard3D />
</>
)
}
else {
return (
<div>No chart selected</div>
)
}
}
export default InputSelecterComponent
export default InputSelecterComponent;

View File

@ -13,7 +13,7 @@ const Visualization = () => {
return (
<div className="visualization-right-sideBar">
<ToggleHeader
options={["Data"]}
options={["Data","Design"]}
activeOption={activeOption}
handleClick={handleToggleClick}
/>

View File

@ -1,324 +1,146 @@
import { useState, useEffect, useRef } from "react";
import React, { useEffect, useState } from "react";
import { ArrowIcon } from "../../../../icons/ExportCommonIcons";
import RegularDropDown from "../../../../ui/inputs/RegularDropDown";
import InputRange from "../../../../ui/inputs/InputRange";
import { useWidgetStore } from "../../../../../store/useWidgetStore";
import ChartComponent from "../../../sidebarLeft/visualization/widgets/ChartComponent";
import RegularDropDown from "../../../../ui/inputs/RegularDropDown";
import { WalletIcon } from "../../../../icons/3dChartIcons";
import SimpleCard from "../../../../../modules/visualization/widgets/floating/cards/SimpleCard";
interface Widget {
id: string;
type?: string;
panel: "top" | "bottom" | "left" | "right";
title?: string;
header?: string;
fontFamily?: string;
fontSize?: string;
fontWeight?: string;
className?: string;
data?: {
labels: string[];
datasets: {
data: number[];
backgroundColor: string;
borderColor: string;
borderWidth: number;
}[];
};
value?: string;
per?: string;
}
const defaultStyle = {
theme: "Glass",
elementColor: "#ffffff",
blurEffect: 10,
opacity: 10,
selectedElement: "Glass",
};
interface ChartElement {
tagName: string;
className: string;
textContent: string;
selector: string;
}
const defaultChartData = {
duration: "1h",
measurements: {},
datasets: [
{
data: [65, 59, 80, 81, 56, 55, 40],
backgroundColor: "#6f42c1",
borderColor: "#b392f0",
borderWidth: 1,
},
],
labels: ["January", "February", "March", "April", "May", "June", "July"],
};
const Design = () => {
const [selectedFont, setSelectedFont] = useState("drop down");
const [selectedSize, setSelectedSize] = useState("drop down");
const [selectedWeight, setSelectedWeight] = useState("drop down");
const [elementColor, setElementColor] = useState("#6f42c1");
const [showColorPicker, setShowColorPicker] = useState(false);
const [chartElements, setChartElements] = useState<ChartElement[]>([]);
const [selectedElementToStyle, setSelectedElementToStyle] = useState<
string | null
>(null);
const [nameInput, setNameInput] = useState("");
const chartRef = useRef<HTMLDivElement>(null);
const { selectedChartId } = useWidgetStore();
const [styles, setStyles] = useState<Record<string, typeof defaultStyle>>({});
const { selectedChartId, setSelectedChartId, widgets, setWidgets } =
useWidgetStore();
const currentStyle = selectedChartId
? styles[selectedChartId.id] || defaultStyle
: defaultStyle;
// Initialize name input and extract elements when selectedChartId changes
useEffect(() => {
setNameInput(selectedChartId?.header || selectedChartId?.title || "");
if (!chartRef.current) return;
const timer = setTimeout(() => {
const chartContainer = chartRef.current;
if (!chartContainer) return;
const elements = Array.from(chartContainer.querySelectorAll("*"))
.filter((el) => {
const tagName = el.tagName.toLowerCase();
return !["script", "style", "meta", "link", "head"].includes(tagName);
})
.map((el, index) => {
const tagName = el.tagName.toLowerCase();
const className =
typeof el.className === "string" ? el.className : "";
const textContent = el.textContent?.trim() || "";
let selector = tagName;
if (className && typeof className === "string") {
const classList = className
.split(/\s+/)
.filter((c) => c.length > 0);
if (classList.length > 0) {
selector += "." + classList.join(".");
}
}
if (!className || className.trim() === "") {
const parent = el.parentElement;
if (parent) {
const siblings = Array.from(parent.children).filter(
(child) => child.tagName.toLowerCase() === tagName
);
const position = siblings.indexOf(el) + 1;
selector += `:nth-of-type(${position})`;
}
}
return {
tagName,
className,
textContent,
selector,
};
});
setChartElements(elements);
}, 300);
return () => clearTimeout(timer);
}, [selectedChartId]);
const applyStyles = () => {
if (!selectedElementToStyle || !chartRef.current) return;
const element = chartRef.current.querySelector(selectedElementToStyle);
if (!element) return;
const elementToStyle = element as HTMLElement;
if (selectedFont !== "drop down") {
elementToStyle.style.fontFamily = selectedFont;
}
if (selectedSize !== "drop down") {
elementToStyle.style.fontSize = selectedSize;
}
if (selectedWeight !== "drop down") {
elementToStyle.style.fontWeight = selectedWeight.toLowerCase();
}
if (elementColor) {
elementToStyle.style.color = elementColor;
}
};
useEffect(() => {
applyStyles();
}, [
selectedFont,
selectedSize,
selectedWeight,
elementColor,
selectedElementToStyle,
]);
const handleUpdateWidget = (updatedProperties: Partial<Widget>) => {
const updateStyle = (updates: Partial<typeof defaultStyle>) => {
if (!selectedChartId) return;
const updatedChartId = {
...selectedChartId,
...updatedProperties,
};
setSelectedChartId(updatedChartId);
const updatedWidgets = widgets.map((widget) =>
widget.id === selectedChartId.id
? { ...widget, ...updatedProperties }
: widget
);
setWidgets(updatedWidgets);
setStyles((prev) => ({
...prev,
[selectedChartId.id]: { ...currentStyle, ...updates },
}));
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newName = e.target.value;
setNameInput(newName);
if (selectedChartId?.title) {
handleUpdateWidget({ title: newName });
} else if (selectedChartId?.header) {
handleUpdateWidget({ header: newName });
}
};
const defaultChartData = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
data: [65, 59, 80, 81, 56, 55, 40],
backgroundColor: "#6f42c1",
borderColor: "#b392f0",
borderWidth: 1,
},
],
};
const elementOptions = chartElements.map((el) => {
let displayName = el.tagName;
if (el.className) displayName += `.${el.className}`;
if (el.textContent)
displayName += ` (${el.textContent.substring(0, 20)}${
el.textContent.length > 20 ? "..." : ""
})`;
return {
display: displayName,
value: el.selector,
};
});
useEffect(() => {
console.log("Styles", styles);
}, [styles]);
return (
<div className="design">
<div className="selectedWidget">
{selectedChartId?.title || selectedChartId?.header || "Widget 1"}
<div className="appearance-container">
<div className="header-container">
<div className="head">Appearance</div>
<div className="icon">
<ArrowIcon />
</div>
</div>
<div className="appearance-style">
<div className="theme-wrapper">
<div className="key">Theme</div>
<div className="value">
<RegularDropDown
header={currentStyle.theme}
options={["Glass", "Fill", "Transparent"]}
onSelect={(theme) => updateStyle({ theme })}
/>
</div>
</div>
{currentStyle.theme === "Glass" && (
<div className="blurEffect-wrapper">
<InputRange
label="Blur Effects"
disabled={false}
value={currentStyle.blurEffect}
min={0}
max={50}
onChange={(blurEffect) => updateStyle({ blurEffect })}
onPointerUp={() => {}}
/>
</div>
)}
{currentStyle.theme !== "Fill" && (
<div className="opacity-wrapper">
<InputRange
label="Opacity"
disabled={false}
value={currentStyle.opacity}
min={0}
max={50}
onChange={(opacity) => updateStyle({ opacity })}
onPointerUp={() => {}}
/>
</div>
)}
<div className="color-wrapper">
<div className="key">Color</div>
<div className="value">
<input
type="color"
value={currentStyle.elementColor}
onChange={(e) => updateStyle({ elementColor: e.target.value })}
/>
<span style={{ marginLeft: "10px" }}>
{currentStyle.elementColor}
</span>
</div>
</div>
</div>
</div>
<div className="reviewChart" ref={chartRef}>
{selectedChartId?.title ? (
<div className="element-container">
<div className="display-element">
<ChartComponent
type={selectedChartId.type || "bar"}
title={selectedChartId.title}
data={selectedChartId.data || defaultChartData}
/>
) : (
<SimpleCard
header={selectedChartId?.header || ""}
icon={WalletIcon}
value={selectedChartId?.value || ""}
per={selectedChartId?.per || ""}
/>
)}
</div>
<div className="optionsContainer">
<div className="option">
<span>Element to Style</span>
<RegularDropDown
header={selectedElementToStyle || "Select Element"}
options={
elementOptions.length > 0
? elementOptions.map((opt) => opt.display)
: ["No elements found"]
}
onSelect={(value) => {
const selected = elementOptions.find(
(opt) => opt.display === value
);
setSelectedElementToStyle(selected?.value || null);
type={selectedChartId?.type ?? "bar"}
title={selectedChartId?.title ?? "Chart"}
data={{
labels: selectedChartId?.data?.labels ?? defaultChartData.labels,
datasets: selectedChartId?.data?.datasets?.length
? selectedChartId.data.datasets
: defaultChartData.datasets,
}}
/>
</div>
<div className="option">
<span>Name</span>
<input
type="text"
value={nameInput}
onChange={handleNameChange}
placeholder="Enter name"
/>
<div className="name-wrapper">
<div className="key">Name</div>
<input className="value" type="text" />
</div>
{selectedChartId?.title && (
<div className="option">
<span>Chart Type</span>
<div className="element-wrapper">
<div className="key">Element</div>
<div className="value">
<RegularDropDown
header={selectedChartId?.type || "Select Type"}
options={["bar", "line", "pie", "doughnut", "radar", "polarArea"]}
onSelect={(value) => {
handleUpdateWidget({ type: value });
}}
header={currentStyle.selectedElement}
options={["Glass", "Fill", "Transparent"]}
onSelect={(selectedElement) => updateStyle({ selectedElement })}
/>
</div>
)}
<div className="option">
<span>Font Family</span>
<RegularDropDown
header={selectedChartId?.fontFamily || "Select Font"}
options={["Arial", "Roboto", "Sans-serif"]}
onSelect={(value) => setSelectedFont(value)}
/>
</div>
<div className="option">
<span>Size</span>
<RegularDropDown
header={selectedChartId?.fontSize || "Select Size"}
options={["12px", "14px", "16px", "18px"]}
onSelect={(value) => setSelectedSize(value)}
/>
</div>
<div className="option">
<span>Weight</span>
<RegularDropDown
header={selectedChartId?.fontWeight || "Select Weight"}
options={["Light", "Regular", "Bold"]}
onSelect={(value) => setSelectedWeight(value)}
/>
</div>
<div className="option">
<div
className="header"
onClick={() => setShowColorPicker((prev) => !prev)}
>
<span>Element Color</span>
<div className="icon"></div>
</div>
{showColorPicker && (
<div className="colorDisplayer">
<input
type="color"
value={elementColor}
onChange={(e) => {
setElementColor(e.target.value);
if (selectedChartId?.data) {
handleUpdateWidget({
data: {
...selectedChartId.data,
datasets: [
{
...selectedChartId.data.datasets[0],
backgroundColor: e.target.value,
},
],
},
});
}
}}
/>
<span style={{ marginLeft: "10px" }}>{elementColor}</span>
</div>
)}
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
import React from "react";
import RenderOverlay from "./Overlay";
import { useSelectedUserStore } from "../../store/useCollabStore";
import { useSelectedUserStore } from "../../store/collaboration/useCollabStore";
import { useCamMode } from "../../store/builder/store";
const FollowPerson: React.FC = () => {

View File

@ -0,0 +1,94 @@
import React, { useState } from "react";
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
interface CommentThreadsProps {
commentClicked: () => void;
}
const CommentThreads: React.FC<CommentThreadsProps> = ({ commentClicked }) => {
const [expand, setExpand] = useState(false);
const commentsedUsers = [{ creatorId: "1" }];
const CommentDetails = {
state: "active",
commentId: "c-1",
creatorId: "12",
createdAt: "2 hours ago",
comment: "Thread check",
lastUpdatedAt: "string",
replies: [
{
replyId: "string",
creatorId: "string",
createdAt: "string",
lastUpdatedAt: "string",
reply: "string",
},
{
replyId: "string",
creatorId: "string",
createdAt: "string",
lastUpdatedAt: "string",
reply: "string",
},
],
};
function getUsername(userId: string) {
const UserName = "username";
return UserName;
}
function getDetails(type?: "clicked") {
if (type === "clicked") {
setExpand(true);
commentClicked();
} else {
setExpand((prev) => !prev);
}
}
return (
<div className="comments-threads-wrapper">
<button
onPointerEnter={() => getDetails()}
onPointerLeave={() => getDetails()}
onClick={() => getDetails("clicked")}
className={`comments-threads-container ${
expand ? "open" : "closed"
} unread`}
>
<div className="users-commented">
{commentsedUsers.map((val, i) => (
<div
className="users"
key={val.creatorId}
style={{
background: getAvatarColor(i, getUsername(val.creatorId)),
}}
>
{getUsername(val.creatorId)[0]}
</div>
))}
</div>
<div className={`last-comment-details ${expand ? "expand" : ""}`}>
<div className="header">
<div className="user-name">
{getUsername(CommentDetails.creatorId)}
</div>
<div className="time">{CommentDetails.createdAt}</div>
</div>
<div className="message">{CommentDetails.comment}</div>
{CommentDetails.replies.length > 0 && (
<div className="replies">
{CommentDetails.replies.length}{" "}
{CommentDetails.replies.length === 1 ? "reply" : "replies"}
</div>
)}
</div>
</button>
</div>
);
};
export default CommentThreads;

View File

@ -0,0 +1,132 @@
import React, { useEffect, useRef, useState } from "react";
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
import { KebabIcon } from "../../icons/ExportCommonIcons";
import { adjustHeight } from "./function/textAreaHeightAdjust";
interface MessageProps {
val: Reply | CommentSchema;
i: number;
}
const Messages: React.FC<MessageProps> = ({ val, i }) => {
const [isEditing, setIsEditing] = useState(false);
const [openOptions, setOpenOptions] = useState(false);
// input
const [value, setValue] = useState<string>(
"reply" in val ? val.reply : val.comment
);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const currentUser = "1";
const UserName = "username";
useEffect(() => {
if (textareaRef.current) adjustHeight(textareaRef.current);
}, [value]);
function handleCancelAction() {
setIsEditing(false);
}
function handleSaveAction() {
setIsEditing(false);
}
function handleDeleteAction() {
setOpenOptions(false);
}
return (
<>
{isEditing ? (
<div className="edit-container">
<div className="input-container">
<textarea
placeholder="type here"
ref={textareaRef}
autoFocus
value={value}
onChange={(e) => setValue(e.target.value)}
style={{ resize: "none" }}
/>
</div>
<div className="actions-container">
<div className="options"></div>
<div className="actions">
<button
className="cancel-button"
onClick={() => {
handleCancelAction();
}}
>
Cancel
</button>
<button
className="save-button"
onClick={() => {
handleSaveAction();
}}
>
Save
</button>
</div>
</div>
</div>
) : (
<div className="message-container">
<div
className="profile"
style={{ background: getAvatarColor(i, UserName) }}
>
{UserName[0]}
</div>
<div className="content">
<div className="user-details">
<div className="user-name">{UserName}</div>
<div className="time">{val.createdAt}</div>
</div>
{val.creatorId === currentUser && (
<div className="more-options">
<button
className="more-options-button"
onClick={() => {
setOpenOptions(!openOptions);
}}
>
<KebabIcon />
</button>
{openOptions && (
<div className="options-list">
<button
className="option"
onClick={() => {
setOpenOptions(false);
setIsEditing(true);
}}
>
Edit
</button>
<button
className="option"
onClick={() => {
handleDeleteAction();
}}
>
Delete
</button>
</div>
)}
</div>
)}
<div className="message">
{"reply" in val ? val.reply : val.comment}
</div>
</div>
</div>
)}
</>
);
};
export default Messages;

View File

@ -0,0 +1,156 @@
import React, { useEffect, useRef, useState } from "react";
import { CloseIcon, KebabIcon } from "../../icons/ExportCommonIcons";
import Messages from "./Messages";
import { ExpandIcon } from "../../icons/SimulationIcons";
import { adjustHeight } from "./function/textAreaHeightAdjust";
const ThreadChat: React.FC = () => {
const [openThreadOptions, setOpenThreadOptions] = useState(false);
const [inputActive, setInputActive] = useState(false);
const [value, setValue] = useState<string>("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const [dragging, setDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [position, setPosition] = useState({ x: 100, y: 100 });
const messages = [
{
replyId: "user 1",
creatorId: "1",
createdAt: "2 hrs ago",
lastUpdatedAt: "2 hrs ago",
reply:
"reply testing reply content 1, reply testing reply content 1reply testing reply content 1",
},
{
replyId: "user 2",
creatorId: "2",
createdAt: "2 hrs ago",
lastUpdatedAt: "2 hrs ago",
reply: "reply 2",
},
];
useEffect(() => {
if (textareaRef.current) adjustHeight(textareaRef.current);
}, [value]);
const clamp = (val: number, min: number, max: number) => {
return Math.min(Math.max(val, min), max);
};
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
if (event.button !== 0) return;
const wrapper = wrapperRef.current;
if (!wrapper) return;
const rect = wrapper.getBoundingClientRect();
const offsetX = event.clientX - rect.left;
const offsetY = event.clientY - rect.top;
setDragging(true);
setDragOffset({ x: offsetX, y: offsetY });
wrapper.setPointerCapture(event.pointerId);
};
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
if (!dragging) return;
const container = document.getElementById("work-space-three-d-canvas");
const wrapper = wrapperRef.current;
if (!container || !wrapper) return;
const containerRect = container.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
let newX = event.clientX - containerRect.left - dragOffset.x;
let newY = event.clientY - containerRect.top - dragOffset.y;
const maxX = containerRect.width - wrapper.offsetWidth;
const maxY = containerRect.height - wrapper.offsetHeight;
newX = clamp(newX, 0, maxX);
newY = clamp(newY, 0, maxY);
setPosition({ x: newX, y: newY });
};
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
if (!dragging) return;
setDragging(false);
const wrapper = wrapperRef.current;
if (wrapper) wrapper.releasePointerCapture(event.pointerId);
};
return (
<div
ref={wrapperRef}
className="thread-chat-wrapper"
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
position: "absolute",
left: position.x,
top: position.y,
cursor: dragging ? "grabbing" : "grab",
userSelect: "none",
zIndex: 9999,
}}
>
<div className="thread-chat-container">
<div className="header-wrapper">
<div className="header">Comment</div>
<div className="header-options">
<button
className="options-button"
onClick={() => setOpenThreadOptions(!openThreadOptions)}
>
<KebabIcon />
</button>
{openThreadOptions && (
<div className="options-list">
<div className="options">Mark as Unread</div>
<div className="options">Mark as Resolved</div>
<div className="options delete">Delete Thread</div>
</div>
)}
<button className="close-button">
<CloseIcon />
</button>
</div>
</div>
<div className="messages-wrapper">
{messages.map((val, i) => (
<Messages val={val as any} i={i} key={val.replyId} />
))}
</div>
<div className="send-message-wrapper">
<div className={`input-container ${inputActive ? "active" : ""}`}>
<textarea
placeholder="type something"
ref={textareaRef}
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setInputActive(true)}
onBlur={() => setInputActive(false)}
style={{ resize: "none" }}
/>
<div className={`sent-button ${value === "" ? "disable-send-btn" : ""}`}>
<ExpandIcon />
</div>
</div>
</div>
</div>
</div>
);
};
export default ThreadChat;

View File

@ -0,0 +1,17 @@
export const adjustHeight = (textareaRef: HTMLTextAreaElement) => {
const el = textareaRef;
if (el) {
el.style.height = "auto";
el.style.height = "38px";
// Clamp to max height for 6 lines
const lineHeight = 18; // px, adjust if needed
const maxHeight = lineHeight * 6;
if (el.scrollHeight > maxHeight) {
el.style.overflowY = "auto";
el.style.height = `${maxHeight}px`;
} else {
el.style.overflowY = "hidden";
}
}
};

View File

@ -8,6 +8,7 @@ import { useSaveVersion } from "../../../store/builder/store";
import Search from "../inputs/Search";
import OuterClick from "../../../utils/outerClick";
import RegularDropDown from "../inputs/RegularDropDown";
import { useProductStore } from "../../../store/simulation/useProductStore";
interface Layout {
id: number;
@ -18,6 +19,8 @@ interface CompareLayoutProps {
}
const CompareLayOut: React.FC<CompareLayoutProps> = ({ dummyLayouts }) => {
const { products } = useProductStore();
console.log('products: ', products);
const [width, setWidth] = useState("50vw");
const [isResizing, setIsResizing] = useState(false);
const [showLayoutDropdown, setShowLayoutDropdown] = useState(false);
@ -141,17 +144,17 @@ const CompareLayOut: React.FC<CompareLayoutProps> = ({ dummyLayouts }) => {
<div className="header">Layouts</div>
<Search onChange={() => {}} />
<div className="layouts-container">
{dummyLayouts.map((layout) => (
{products.map((layout) => (
<button
key={layout.id}
key={layout.productId}
className="layout-wrapper"
onClick={() => {
handleSelectLayout(layout.name);
handleSelectLayout(layout.productName);
setShowLayoutDropdown(false);
}}
>
<LayoutIcon />
<div className="layout">{layout.name}</div>
<div className="layout">{layout.productName}</div>
</button>
))}
</div>
@ -165,7 +168,7 @@ const CompareLayOut: React.FC<CompareLayoutProps> = ({ dummyLayouts }) => {
<div className="selectLayout-wrapper">
<RegularDropDown
header={selectedLayout}
options={dummyLayouts.map((l) => l.name)} // Pass layout names as options
options={products.map((l) => l.productName)} // Pass layout names as options
onSelect={handleSelectLayout}
search={false}
/>

View File

@ -14,15 +14,15 @@ const ToggleHeader: React.FC<ToggleHeaderProps> = ({
return (
<div className="toggle-header-container">
{options.map((option, index) => (
<div
key={index}
<button
key={`${index}-${option}`}
className={`toggle-header-item ${
option === activeOption ? "active" : ""
}`}
onClick={() => handleClick(option)} // Call handleClick when an option is clicked
>
{option}
</div>
</button>
))}
</div>
);

View File

@ -128,7 +128,7 @@ const DropDownList: React.FC<DropDownListProps> = ({
title="collapse-btn"
className="collapse-icon option"
style={{ transform: isOpen ? "rotate(0deg)" : "rotate(-90deg)" }}
onClick={handleToggle}
// onClick={handleToggle}
>
<ArrowIcon />
</button>

View File

@ -1,14 +1,18 @@
// LogList.tsx
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { LogListIcon, CloseIcon } from "../../icons/ExportCommonIcons"; // Adjust path as needed
import { useLogger } from "./LoggerContext";
import { GetLogIcon } from "../../footer/getLogIcons";
const LogList: React.FC = () => {
const { logs, clear, setIsLogListVisible } = useLogger();
const [selectedTab, setSelectedTab] = useState<
"all" | "info" | "warning" | "error" | "log" | "success"
>("all");
const {
logs,
clear,
setIsLogListVisible,
isLogListVisible,
selectedTab,
setSelectedTab,
} = useLogger();
const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString();
@ -17,6 +21,19 @@ const LogList: React.FC = () => {
? [...logs].reverse()
: [...logs].filter((log) => log.type === selectedTab).reverse();
useEffect(() => {
if (isLogListVisible && logs.length > 0) {
const lastLog = logs[logs.length - 1];
const validTypes = ["all", "info", "warning", "error"];
if (validTypes.includes(lastLog.type)) {
setSelectedTab(lastLog.type);
} else {
setSelectedTab("all");
}
}
}, [isLogListVisible]);
return (
// eslint-disable-next-line
<div

View File

@ -1,5 +1,12 @@
// LoggerProvider.tsx
import React, { createContext, useContext, useState, useCallback, useMemo, useEffect } from "react";
import React, {
createContext,
useContext,
useState,
useCallback,
useMemo,
useEffect,
} from "react";
import { MathUtils } from "three";
export type LogType = "log" | "info" | "warning" | "error" | "success";
@ -16,6 +23,8 @@ interface LoggerContextValue {
setLogs: React.Dispatch<React.SetStateAction<LogEntry[]>>;
isLogListVisible: boolean;
setIsLogListVisible: React.Dispatch<React.SetStateAction<boolean>>;
selectedTab: LogType | "all";
setSelectedTab: React.Dispatch<React.SetStateAction<LogType | "all">>;
log: (message: string) => void;
info: (message: string) => void;
warn: (message: string) => void;
@ -31,19 +40,17 @@ export const LoggerProvider: React.FC<{ children: React.ReactNode }> = ({
}) => {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [isLogListVisible, setIsLogListVisible] = useState<boolean>(false);
const [selectedTab, setSelectedTab] = useState<LogType | "all">("all");
const addLog = useCallback(
(type: LogType, message: string) => {
const newLog: LogEntry = {
id: MathUtils.generateUUID(),
type,
message,
timestamp: new Date(),
};
setLogs((prevLogs) => [...prevLogs, newLog]);
},
[]
);
const addLog = useCallback((type: LogType, message: string) => {
const newLog: LogEntry = {
id: MathUtils.generateUUID(),
type,
message,
timestamp: new Date(),
};
setLogs((prevLogs) => [...prevLogs, newLog]);
}, []);
const loggerMethods: LoggerContextValue = useMemo(
() => ({
@ -51,17 +58,33 @@ export const LoggerProvider: React.FC<{ children: React.ReactNode }> = ({
setLogs,
isLogListVisible,
setIsLogListVisible,
selectedTab,
setSelectedTab,
log: (message: string) => addLog("log", message),
info: (message: string) => addLog("info", message),
warn: (message: string) => addLog("warning", message),
error: (message: string) => addLog("error", message),
success: (message: string) => addLog("success", message),
clear: () => setLogs([]),
clear: () => {
if (selectedTab !== "all") {
setLogs((prevLogs) =>
prevLogs.filter((log) => log.type !== selectedTab)
);
} else {
setLogs([]);
}
},
}),
[logs, setLogs, isLogListVisible, setIsLogListVisible, addLog]
[
logs,
setLogs,
isLogListVisible,
setIsLogListVisible,
selectedTab,
setSelectedTab,
addLog,
]
);
// Attach logger globally to window object
useEffect(() => {
(window as any).echo = {
log: loggerMethods.log,

View File

@ -7,6 +7,7 @@ import useVersionHistoryStore, {
useVersionStore,
} from "../../../store/builder/store";
import { useSubModuleStore } from "../../../store/useModuleStore";
import { generateUniqueId } from "../../../functions/generateUniqueId";
interface MenuBarProps {
setOpenMenu: (isOpen: boolean) => void;
@ -69,7 +70,7 @@ const MenuBar: React.FC<MenuBarProps> = ({ setOpenMenu }) => {
const versionCount = versionStore.versions.length;
const newVersion = {
id: crypto.randomUUID(),
id: generateUniqueId(),
versionLabel: `v${versionCount + 1}.0`,
timestamp: `${new Date().toLocaleTimeString("en-US", {
hour: "numeric",

View File

@ -10,7 +10,7 @@ function updateDistanceText(
////////// Updating the Distance Texts of the lines that are affected during drag //////////
const DistanceGroup = scene.children.find((child) => child.name === "Distance_Text") as THREE.Group;
const DistanceGroup = scene.getObjectByName('Distance_Text') as THREE.Group;
affectedLines.forEach((lineIndex) => {
const mesh = floorPlanGroupLine.current.children[lineIndex] as THREE.Mesh;

View File

@ -1,18 +1,19 @@
import * as THREE from "three";
import { useEffect, useRef, useState } from "react";
import { useFrame } from "@react-three/fiber";
import { useFrame, useThree } from "@react-three/fiber";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import camModel from "../../../assets/gltf-glb/camera face 2.gltf";
import getActiveUsersData from "../../../services/factoryBuilder/collab/getActiveUsers";
import { useActiveUsers, useSocketStore } from "../../../store/builder/store";
import { useActiveUsers, useCamMode, useSocketStore } from "../../../store/builder/store";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import { useNavigate } from "react-router-dom";
import { Html } from "@react-three/drei";
import CollabUserIcon from "./collabUserIcon";
import useModuleStore from "../../../store/useModuleStore";
import { getAvatarColor } from "../functions/getAvatarColor";
import { useSelectedUserStore } from "../../../store/useCollabStore";
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import setCameraView from "../functions/setCameraView";
const CamModelsGroup = () => {
const navigate = useNavigate();
@ -30,6 +31,28 @@ const CamModelsGroup = () => {
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
loader.setDRACOLoader(dracoLoader);
const { camMode } = useCamMode();
const { camera, controls } = useThree(); // Access R3F camera and controls
useEffect(() => {
if (camMode !== "FollowPerson") return;
// If a user is selected, set the camera view to their location
// and update the camera and controls accordingly
if (selectedUser?.location) {
const { position, rotation, target } = selectedUser.location;
if (rotation && target)
setCameraView({
controls,
camera,
position,
rotation,
target,
username: selectedUser.name,
});
}
}, [selectedUser, camera, controls, camMode]);
const [cams, setCams] = useState<any[]>([]);
const [models, setModels] = useState<
Record<
@ -260,11 +283,10 @@ const CamModelsGroup = () => {
textAlign: "center",
fontFamily: "Arial, sans-serif",
display: `${activeModule !== "visualization" ? "" : "none"}`,
opacity: `${
selectedUser?.name !== cam.userData.userName && !isPlaying
? 1
: 0
}`,
opacity: `${selectedUser?.name !== cam.userData.userName && !isPlaying
? 1
: 0
}`,
transition: "opacity .2s ease",
}}
position={[-0.015, 0, 0.7]}

View File

@ -1,6 +1,6 @@
import React from "react";
import CustomAvatar from "../users/Avatar";
import { useSelectedUserStore } from "../../../store/useCollabStore";
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
import { useCamMode } from "../../../store/builder/store";
interface CollabUserIconProps {

View File

@ -1,34 +1,18 @@
import React, { useEffect } from "react";
import React from "react";
import CamModelsGroup from "./camera/collabCams";
import { useSelectedUserStore } from "../../store/useCollabStore";
import { useThree } from "@react-three/fiber";
import setCameraView from "./functions/setCameraView";
import { useCamMode } from "../../store/builder/store";
import CommentsGroup from "./comments/commentsGroup";
const Collaboration: React.FC = () => {
const { selectedUser } = useSelectedUserStore();
const { camMode } = useCamMode();
const { camera, controls } = useThree(); // Access R3F camera and controls
useEffect(() => {
if (camMode !== "FollowPerson") return;
// If a user is selected, set the camera view to their location
// and update the camera and controls accordingly
if (selectedUser?.location) {
const { position, rotation, target } = selectedUser.location;
if (rotation && target)
setCameraView({
controls,
camera,
position,
rotation,
target,
username: selectedUser.name,
});
}
}, [selectedUser, camera, controls, camMode]);
return (
<>
return <CamModelsGroup />;
<CamModelsGroup />
<CommentsGroup />
</>
);
};
export default Collaboration;

View File

@ -0,0 +1,124 @@
import { useEffect, useState } from "react";
import { useActiveTool } from "../../../store/builder/store"
import { useThree } from "@react-three/fiber";
import { MathUtils, Vector3 } from "three";
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
import CommentInstances from "./instances/commentInstances";
import { Sphere } from "@react-three/drei";
function CommentsGroup() {
const { gl, raycaster, camera, scene, pointer } = useThree();
const { activeTool } = useActiveTool();
const { addComment } = useCommentStore();
const [hoverPos, setHoverPos] = useState<Vector3 | null>(null);
const userId = localStorage.getItem('userId') ?? '';
useEffect(() => {
const canvasElement = gl.domElement;
let drag = false;
let isLeftMouseDown = false;
const onMouseDown = (evt: MouseEvent) => {
if (evt.button === 0) {
isLeftMouseDown = true;
drag = false;
}
};
const onMouseUp = (evt: MouseEvent) => {
if (evt.button === 0) {
isLeftMouseDown = false;
}
}
const onMouseMove = () => {
if (isLeftMouseDown) {
drag = true;
}
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.name.includes("commentHolder") &&
!intersect.object.name.includes("agv-collider") &&
!(intersect.object.type === "GridHelper")
);
if (intersects.length > 0) {
const point = intersects[0].point;
setHoverPos(new Vector3(point.x, Math.max(point.y, 0), point.z));
} else {
setHoverPos(null);
}
};
const onMouseClick = () => {
if (drag) return;
const intersects = raycaster
.intersectObjects(scene.children, true)
.filter(
(intersect) =>
!intersect.object.name.includes("Roof") &&
!intersect.object.name.includes("MeasurementReference") &&
!intersect.object.name.includes("commentHolder") &&
!intersect.object.name.includes("agv-collider") &&
!(intersect.object.type === "GridHelper")
);
if (intersects.length > 0) {
const position = new Vector3(intersects[0].point.x, Math.max(intersects[0].point.y, 0), intersects[0].point.z);
const comment: CommentSchema = {
state: 'active',
commentId: MathUtils.generateUUID(),
creatorId: userId,
createdAt: new Date().toISOString(),
comment: '',
lastUpdatedAt: new Date().toISOString(),
position: position.toArray(),
rotation: [0, 0, 0],
replies: []
}
addComment(comment);
setHoverPos(null);
}
}
if (activeTool === 'comment') {
canvasElement.addEventListener("mousedown", onMouseDown);
canvasElement.addEventListener("mouseup", onMouseUp);
canvasElement.addEventListener("mousemove", onMouseMove);
canvasElement.addEventListener("click", onMouseClick);
} else {
setHoverPos(null);
}
return () => {
canvasElement.removeEventListener("mousedown", onMouseDown);
canvasElement.removeEventListener("mouseup", onMouseUp);
canvasElement.removeEventListener("mousemove", onMouseMove);
canvasElement.removeEventListener("click", onMouseClick);
};
}, [activeTool, camera])
return (
<>
<CommentInstances />
{hoverPos && (
<Sphere name={'commentHolder'} args={[0.1, 16, 16]} position={hoverPos}>
<meshStandardMaterial color="orange" />
</Sphere>
)}
</>
)
}
export default CommentsGroup

View File

@ -0,0 +1,64 @@
import { Html, TransformControls } from '@react-three/drei';
import { useEffect, useRef, useState } from 'react'
import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys';
import CommentThreads from '../../../../../components/ui/collaboration/CommentThreads';
function CommentInstance({ comment }: { comment: CommentSchema }) {
const { isPlaying } = usePlayButtonStore();
const CommentRef = useRef(null);
const [selectedComment, setSelectedComment] = useState<CommentSchema | null>(null);
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const keyCombination = detectModifierKeys(e);
if (!selectedComment) return;
if (keyCombination === "G") {
setTransformMode((prev) => (prev === "translate" ? null : "translate"));
}
if (keyCombination === "R") {
setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedComment]);
const commentClicked = () => {
console.log('hii');
setSelectedComment(comment);
}
if (comment.state === 'inactive' || isPlaying) return null;
return (
<>
<Html
ref={CommentRef}
zIndexRange={[1, 0]}
prepend
sprite
center
position={comment.position}
rotation={comment.rotation}
className='comments-main-wrapper'
>
<CommentThreads commentClicked={commentClicked} />
</Html>
{CommentRef.current && transformMode && (
<TransformControls
object={CommentRef.current}
mode={transformMode}
onMouseUp={(e) => {
}}
/>
)}
</>
)
}
export default CommentInstance;

View File

@ -0,0 +1,23 @@
import React, { useEffect } from 'react'
import CommentInstance from './commentInstance/commentInstance'
import { useCommentStore } from '../../../../store/collaboration/useCommentStore'
function CommentInstances() {
const { comments } = useCommentStore();
useEffect(() => {
// console.log('comments: ', comments);
}, [comments])
return (
<>
{comments.map((comment: CommentSchema) => (
<React.Fragment key={comment.commentId}>
<CommentInstance comment={comment} />
</React.Fragment>
))}
</>
)
}
export default CommentInstances

View File

@ -5,7 +5,6 @@ import Controls from '../controls/controls';
import { Environment } from '@react-three/drei'
import background from "../../../assets/textures/hdr/mudroadpuresky2k.hdr";
import { MovingClouds } from '../clouds/clouds';
function Setup() {
return (
@ -18,7 +17,7 @@ function Setup() {
<PostProcessing />
<MovingClouds />
{/* <MovingClouds /> */}
<Environment files={background} environmentIntensity={1.5} />
</>

View File

@ -218,7 +218,7 @@ const MeasurementTool = () => {
sprite
>
<div>
{startConePosition.distanceTo(endConePosition).toFixed(2)} m
{(startConePosition.distanceTo(endConePosition) + (coneSize.height)).toFixed(2)} m
</div>
</Html>
)}

View File

@ -1,6 +1,7 @@
import * as THREE from "three";
import { useMemo, useRef } from "react";
import { useMemo, useRef, useState } from "react";
import { useThree } from "@react-three/fiber";
import { useDeleteTool } from "../../../../store/builder/store";
interface ConnectionLine {
id: string;
@ -10,8 +11,10 @@ interface ConnectionLine {
}
export function Arrows({ connections }: { connections: ConnectionLine[] }) {
const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null);
const groupRef = useRef<THREE.Group>(null);
const { scene } = useThree();
const { deleteTool } = useDeleteTool();
const getWorldPositionFromScene = (uuid: string): THREE.Vector3 | null => {
const obj = scene.getObjectByProperty("uuid", uuid);
@ -52,11 +55,25 @@ export function Arrows({ connections }: { connections: ConnectionLine[] }) {
return (
<group key={key}>
<mesh geometry={shaftGeometry}>
<meshStandardMaterial color="#42a5f5" />
<mesh
geometry={shaftGeometry}
onPointerOver={() => setHoveredLineKey(key)}
onPointerOut={() => setHoveredLineKey(null)}
>
<meshStandardMaterial
color={deleteTool && hoveredLineKey === key ? "red" : "#42a5f5"}
/>
</mesh>
<mesh position={end} quaternion={rotation} geometry={headGeometry}>
<meshStandardMaterial color="#42a5f5" />
<mesh
position={end}
quaternion={rotation}
geometry={headGeometry}
onPointerOver={() => setHoveredLineKey(key)}
onPointerOut={() => setHoveredLineKey(null)}
>
<meshStandardMaterial
color={deleteTool && hoveredLineKey === key ? "red" : "#42a5f5"}
/>
</mesh>
</group>
);

View File

@ -98,8 +98,6 @@ export const DraggableWidget = ({
const deleteSelectedChart = async () => {
try {
console.log("delete");
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
let deleteWidget = {
@ -111,7 +109,6 @@ export const DraggableWidget = ({
if (visualizationSocket) {
setSelectedChartId(null);
visualizationSocket.emit("v2:viz-widget:delete", deleteWidget);
console.log("delete widget", selectedChartId);
}
const updatedWidgets = selectedZone.widgets.filter(
(w: Widget) => w.id !== widget.id
@ -313,7 +310,6 @@ export const DraggableWidget = ({
ref={chartWidget}
onClick={() => {
setSelectedChartId(widget);
console.log("click");
}}
>
{/* Kebab Icon */}

View File

@ -31,6 +31,7 @@ interface ProductionCapacityProps {
type: string;
position: [number, number, number];
rotation: [number, number, number];
scale?: [number, number, number];
Data?: any;
onContextMenu?: (event: React.MouseEvent) => void;
// onPointerDown:any
@ -41,7 +42,8 @@ const ProductionCapacity: React.FC<ProductionCapacityProps> = ({
type,
Data,
position,
rotation,
rotation = [0, 0, 0],
scale = [0.5, 0.5, 0.5],
onContextMenu,
}) => {
const { selectedChartId, setSelectedChartId } = useWidgetStore();
@ -198,74 +200,75 @@ const ProductionCapacity: React.FC<ProductionCapacityProps> = ({
useEffect(() => { }, [rotation]);
return (
<Html
// data
position={position}
scale={[0.5, 0.5, 0.5]}
rotation={rotation}
// class
wrapperClass="pointer-none"
// other
transform
zIndexRange={[1, 0]}
prepend
// sprite
distanceFactor={20}
// events
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onDrop={(e) => {
e.preventDefault();
}}
>
<div
className={`productionCapacity-wrapper card ${selectedChartId?.id === id ? "activeChart" : ""
}`}
onClick={() => setSelectedChartId({ id: id, type: type })}
onContextMenu={onContextMenu}
style={{
width: "300px", // Original width
height: "300px", // Original height
// transform: transformStyle.transform,
transformStyle: "preserve-3d",
position: "absolute",
transform: "translate(-50%, -50%)",
<>
{position && scale && rotation && <Html
// data
position={position}
scale={scale}
rotation={rotation}
// class
wrapperClass="pointer-none"
// other
transform
zIndexRange={[1, 0]}
prepend
// sprite
distanceFactor={20}
// events
onDragOver={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onDrop={(e) => {
e.preventDefault();
}}
>
<div className="headeproductionCapacityr-wrapper">
<div className="header">Production Capacity</div>
<div className="production-capacity">
<div className="value">1,200</div>{" "}
<div className="value">units/hour</div>
</div>
<div className="production-capacity">
<div className="current">
<div className="key">Current</div>
<div className="value">1500</div>
<div
className={`productionCapacity-wrapper card ${selectedChartId?.id === id ? "activeChart" : ""
}`}
onClick={() => setSelectedChartId({ id: id, type: type })}
onContextMenu={onContextMenu}
style={{
width: "300px", // Original width
height: "300px", // Original height
// transform: transformStyle.transform,
transformStyle: "preserve-3d",
position: "absolute",
transform: "translate(-50%, -50%)",
}}
>
<div className="headeproductionCapacityr-wrapper">
<div className="header">Production Capacity</div>
<div className="production-capacity">
<div className="value">1,200</div>{" "}
<div className="value">units/hour</div>
</div>
<div className="target">
<div className="key">Target</div>
<div className="value">2.345</div>
<div className="production-capacity">
<div className="current">
<div className="key">Current</div>
<div className="value">1500</div>
</div>
<div className="target">
<div className="key">Target</div>
<div className="value">2.345</div>
</div>
{/* <div className="value">units/hour</div> */}
</div>
{/* <div className="value">units/hour</div> */}
</div>{" "}
<div className="bar-chart charts">
{/* Bar Chart */}
<Bar
data={
Object.keys(measurements).length > 0
? chartData
: defaultChartData
}
options={chartOptions}
/>
</div>
</div>{" "}
<div className="bar-chart charts">
{/* Bar Chart */}
<Bar
data={
Object.keys(measurements).length > 0
? chartData
: defaultChartData
}
options={chartOptions}
/>
</div>
</div>
</Html>
</Html>}
</>
);
};

View File

@ -12,12 +12,11 @@ import {
} from "../../../../../components/icons/3dChartIcons";
const TotalCardComponent = ({ object }: any) => {
console.log('object: ', object);
const [progress, setProgress] = useState<any>(0);
const [measurements, setmeasurements] = useState<any>({});
const [duration, setDuration] = useState("1h");
const [name, setName] = useState(object.header ? object.header : "");
console.log('name: ', name);
const email = localStorage.getItem("email") || "";
const organization = email?.split("@")[1]?.split(".")[0];
const { header, flotingDuration, flotingMeasurements } = useChartStore();

View File

@ -21,7 +21,7 @@ import { usePlayButtonStore } from "../store/usePlayButtonStore";
import MarketPlace from "../modules/market/MarketPlace";
import LoadingPage from "../components/templates/LoadingPage";
import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys";
import { useSelectedUserStore } from "../store/useCollabStore";
import { useSelectedUserStore } from "../store/collaboration/useCollabStore";
import FollowPerson from "../components/templates/FollowPerson";
import Scene from "../modules/scene/scene";
import { createHandleDrop } from "../modules/visualization/functions/handleUiDrop";
@ -34,10 +34,11 @@ import Footer from "../components/footer/Footer";
import SelectFloorPlan from "../components/temporary/SelectFloorPlan";
import ControlsPlayer from "../components/layout/controls/ControlsPlayer";
import CompareLayOut from "../components/ui/compareVersion/CompareLayOut";
import {useToggleStore} from "../store/useUIToggleStore";
import { useToggleStore } from "../store/useUIToggleStore";
import RegularDropDown from "../components/ui/inputs/RegularDropDown";
import VersionSaved from "../components/layout/sidebarRight/versionHisory/VersionSaved";
import SimulationPlayer from "../components/ui/simulation/simulationPlayer";
import { useProductStore } from "../store/simulation/useProductStore";
const Project: React.FC = () => {
let navigate = useNavigate();
@ -52,6 +53,7 @@ const Project: React.FC = () => {
const { setWallItems } = useWallItems();
const { setZones } = useZones();
const { isVersionSaved } = useSaveVersion();
const { products } = useProductStore();
useEffect(() => {
if (!isVersionSaved) {
@ -171,7 +173,7 @@ const Project: React.FC = () => {
<div className="initial-selectLayout-wrapper">
<RegularDropDown
header={selectedLayout ?? "Layout 1"}
options={dummyLayouts.map((l) => l.name)} // Pass layout names as options
options={products.map((l) => l.productName)} // Pass layout names as options
onSelect={handleSelectLayout}
search={false}
/>

View File

@ -548,7 +548,7 @@ interface CompareStore {
}
export const useCompareStore = create<CompareStore>((set) => ({
comparePopUp: true,
comparePopUp: false,
setComparePopUp: (value) => set({ comparePopUp: value }),
toggleComparePopUp: () =>
set((state) => ({ comparePopUp: !state.comparePopUp })),

View File

@ -1,36 +1,36 @@
import { create } from 'zustand';
interface SelectedUser {
color: string;
name: string;
id: string,
location?: {
position: {
x: number;
y: number;
z: number;
};
rotation?: {
x: number;
y: number;
z: number;
};
target?: {
x: number;
y: number;
z: number;
};
}
}
interface SelectedUserStore {
selectedUser: SelectedUser | null;
setSelectedUser: (user: SelectedUser) => void;
clearSelectedUser: () => void;
}
export const useSelectedUserStore = create<SelectedUserStore>((set) => ({
selectedUser: null,
setSelectedUser: (user) => set({ selectedUser: user }),
clearSelectedUser: () => set({ selectedUser: null }),
}));
import { create } from 'zustand';
interface SelectedUser {
color: string;
name: string;
id: string,
location?: {
position: {
x: number;
y: number;
z: number;
};
rotation?: {
x: number;
y: number;
z: number;
};
target?: {
x: number;
y: number;
z: number;
};
}
}
interface SelectedUserStore {
selectedUser: SelectedUser | null;
setSelectedUser: (user: SelectedUser) => void;
clearSelectedUser: () => void;
}
export const useSelectedUserStore = create<SelectedUserStore>((set) => ({
selectedUser: null,
setSelectedUser: (user) => set({ selectedUser: user }),
clearSelectedUser: () => set({ selectedUser: null }),
}));

View File

@ -0,0 +1,92 @@
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface CommentStore {
comments: CommentsSchema;
// Comment operations
addComment: (comment: CommentSchema) => void;
setComments: (comments: CommentsSchema) => void;
updateComment: (commentId: string, updates: Partial<CommentSchema>) => void;
removeComment: (commentId: string) => void;
// Reply operations
addReply: (commentId: string, reply: Reply) => void;
updateReply: (commentId: string, replyId: string, updates: Partial<Reply>) => void;
removeReply: (commentId: string, replyId: string) => void;
// Getters
getCommentById: (commentId: string) => CommentSchema | undefined;
}
export const useCommentStore = create<CommentStore>()(
immer((set, get) => ({
comments: [],
// Comment operations
addComment: (comment) => {
set((state) => {
if (!state.comments.find(c => c.commentId === comment.commentId)) {
state.comments.push(JSON.parse(JSON.stringify(comment)));
}
});
},
setComments: (comments) => {
set((state) => {
state.comments = comments;
});
},
updateComment: (commentId, updates) => {
set((state) => {
const comment = state.comments.find(c => c.commentId === commentId);
if (comment) {
Object.assign(comment, updates);
}
});
},
removeComment: (commentId) => {
set((state) => {
state.comments = state.comments.filter(c => c.commentId !== commentId);
});
},
// Reply operations
addReply: (commentId, reply) => {
set((state) => {
const comment = state.comments.find(c => c.commentId === commentId);
if (comment) {
comment.replies.push(reply);
}
});
},
updateReply: (commentId, replyId, updates) => {
set((state) => {
const comment = state.comments.find(c => c.commentId === commentId);
if (comment) {
const reply = comment.replies.find(r => r.replyId === replyId);
if (reply) {
Object.assign(reply, updates);
}
}
});
},
removeReply: (commentId, replyId) => {
set((state) => {
const comment = state.comments.find(c => c.commentId === commentId);
if (comment) {
comment.replies = comment.replies.filter(r => r.replyId !== replyId);
}
});
},
// Getter
getCommentById: (commentId) => {
return get().comments.find(c => c.commentId === commentId);
},
}))
);

View File

@ -3,7 +3,8 @@
// global input style
input {
input,
textarea {
width: 100%;
padding: 4px 8px;
border-radius: #{$border-radius-large};

View File

@ -6,6 +6,10 @@
top: 100px;
left: 40px;
z-index: 10;
.regularDropdown-container {
background: var(--background-color);
}
}
.compareLayOut-wrapper {
@ -28,6 +32,10 @@
position: absolute;
top: 100px;
right: 40px;
.regularDropdown-container {
background: var(--background-color);
}
}
.chooseLayout-container {
@ -158,11 +166,16 @@
background-color: var(--highlight-text-color) !important;
border-radius: 4px;
.layout {
color: var(--text-button-color) !important;
}
svg {
path {
fill: var(--background-color-accent);
fill: var(--text-button-color) !important;
}
}

View File

@ -829,29 +829,122 @@
display: flex;
flex-direction: column;
gap: 15px;
padding: 0;
font-size: var(--font-weight-regular);
color: #4a4a4a;
color: var(--text-color);
padding: 12px;
.reviewChart {
width: 100%;
.appearance-container,
.element-container {
.floating {
width: 100%;
background: var(--background-color);
backdrop-filter: blur(20px);
border-radius: 15px;
outline: 1px solid var(--border-color);
padding: 10px;
display: flex;
flex-direction: column;
gap: 12px;
.header-container {
padding: 0;
height: auto;
}
.appearance-style {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: column;
gap: 12px;
.regularDropdown-container {
.dropdown-options {
width: 130%;
left: -15%;
}
.dropdown-header {
gap: 12px;
}
}
.color-wrapper,
.opacity-wrapper,
.blurEffect-wrapper,
.theme-wrapper {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: column;
.input-range-container {
width: 100%;
padding: 0;
.input-container {}
}
}
.theme-wrapper {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.color-wrapper {
flex-direction: row;
.value {
display: flex;
align-items: center;
input {
width: 34px;
height: 24px;
border-radius: 12px;
padding: 0;
}
}
}
}
}
.selectedWidget {
padding: 6px 12px;
border-top: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
.element-container {
padding: 8px;
.display-element {
width: 100%;
height: 150px;
background: var(--background-color);
backdrop-filter: blur(20px);
border-radius: 5px;
outline: 1px solid var(--border-color);
}
.name-wrapper,
.element-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
.value {
width: 60%;
}
}
}
}
.reviewChart {
width: 100%;
height: 150px;
background: #f0f0f0;
background: var(--background-color);
display: flex;
align-items: center;
}
@ -888,6 +981,7 @@
justify-content: start;
align-items: center;
input[type="color"] {
border: none;
outline: none;
@ -895,6 +989,7 @@
width: 24px;
height: 26px;
border-radius: #{$border-radius-small};
padding: 0;
}
}
}
@ -1749,14 +1844,18 @@
bottom: 45px;
right: 10px;
z-index: 10;
background: var(--background-color);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 18px;
.versionSaved-wrapper {
border-radius: 20px;
padding: 8px 10px;
background: var(--background-color);
backdrop-filter: blur(20px);
}
.version-header {
display: flex;
justify-content: space-between;
@ -1820,7 +1919,8 @@
display: none;
}
.edit-version-popup-wrapper {
.edit-version-popup-wrapper,
.finishEdit-version-popup-wrapper {
height: 100vh;
@ -1828,14 +1928,14 @@
background: var(--background-color-secondary);
@include flex-center;
.details-wrapper-popup-container {
.details-wrapper-popup-container,
.finishEdit-wrapper-popup-container {
min-width: 535px;
width: 520px;
background: var(--background-color);
border-radius: #{$border-radius-large};
backdrop-filter: blur(15px);
outline: 1px solid var(--border-color);
padding: 6px;
display: flex;
flex-direction: column;
@ -1844,6 +1944,7 @@
.header-wrapper {
display: flex;
align-items: center;
gap: 6px;
}
@ -1912,4 +2013,19 @@
}
}
}
.finishEdit-wrapper-popup-container {
min-height: 250px;
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
.versionname {
font-size: var(--font-size-large);
color: var(--background-color-accent);
color: #CCACFF;
text-transform: capitalize;
}
}
}

View File

@ -1,48 +1,48 @@
// abstracts
@use 'abstracts/variables';
@use 'abstracts/mixins';
@use 'abstracts/functions';
@use "abstracts/variables";
@use "abstracts/mixins";
@use "abstracts/functions";
// base
@use 'base/reset';
@use 'base/typography';
@use 'base/global';
@use 'base/base';
@use "base/reset";
@use "base/typography";
@use "base/global";
@use "base/base";
// components
@use 'components/button';
@use 'components/form';
@use 'components/input';
@use 'components/lists';
@use 'components/moduleToggle';
@use 'components/templates';
@use 'components/tools';
@use 'components/visualization/floating/energyConsumed';
@use 'components/visualization/ui/styledWidgets';
@use 'components/visualization/floating/common';
@use 'components/marketPlace/marketPlace';
@use 'components/menu/menu';
@use 'components/confirmationPopUp';
@use 'components/simulation/simulation';
@use 'components/simulation/analysis';
@use 'components/logs/logs';
@use 'components/footer/footer.scss';
@use "components/button";
@use "components/form";
@use "components/input";
@use "components/lists";
@use "components/moduleToggle";
@use "components/templates";
@use "components/tools";
@use "components/visualization/floating/energyConsumed";
@use "components/visualization/ui/styledWidgets";
@use "components/visualization/floating/common";
@use "components/marketPlace/marketPlace";
@use "components/menu/menu";
@use "components/confirmationPopUp";
@use "components/simulation/simulation";
@use "components/simulation/analysis";
@use "components/logs/logs";
@use "components/footer/footer.scss";
// layout
@use 'layout/loading';
@use 'layout/sidebar';
@use 'layout/popup';
@use 'layout/toast';
@use 'layout/skeleton';
@use 'layout/compareLayoutPopUp';
@use 'layout/compareLayout';
@use "layout/loading";
@use "layout/sidebar";
@use "layout/popup";
@use "layout/toast";
@use "layout/skeleton";
@use "layout/compareLayoutPopUp";
@use "layout/compareLayout";
// pages
@use 'pages/dashboard';
@use 'pages/home';
@use 'pages/realTimeViz';
@use 'pages/userAuth';
@use "pages/dashboard";
@use "pages/home";
@use "pages/realTimeViz";
@use "pages/userAuth";
//
@use './scene/scene'
//
@use "./scene/scene";
@use "./scene/comments";

View File

@ -0,0 +1,310 @@
@use "../abstracts/variables" as *;
@use "../abstracts/mixins" as *;
.comments-main-wrapper {
position: relative;
}
.comments-threads-wrapper {
position: absolute;
top: 0;
left: 0;
padding: 4px;
background: var(--background-color);
border-radius: #{$border-radius-extra-large} #{$border-radius-extra-large} #{$border-radius-extra-large}
0;
backdrop-filter: blur(12px);
z-index: 1000;
transform: translateY(-100%);
outline: 1px solid var(--border-color);
.comments-threads-container {
display: flex;
align-items: start;
flex-direction: column;
.users-commented {
@include flex-center;
.users {
height: 24px;
line-height: 24px;
width: 24px;
text-transform: uppercase;
border-radius: 50%;
}
}
.last-comment-details {
display: flex;
align-items: start;
flex-direction: column;
overflow: hidden;
transition: all 0.2s ease-in;
.header {
@include flex-center;
gap: 10px;
.user-name {
text-transform: capitalize;
}
.time {
font-size: var(--font-size-small);
color: var(--input-text-color);
}
}
.message {
margin-top: 10px;
}
.replies {
margin-top: 4px;
font-size: var(--font-size-small);
color: var(--input-text-color);
}
.header,
.message,
.replies {
display: none;
opacity: 0;
}
}
.expand {
min-width: 200px;
max-width: 260px;
padding: 12px;
padding-top: 0;
height: 100%;
}
}
.open {
.users-commented {
padding: 12px;
}
.header,
.message,
.replies {
display: flex !important;
opacity: 1 !important;
}
}
}
.thread-chat-wrapper {
position: absolute;
// remove later
top: 50%;
left: 50%;
// ----
z-index: #{$z-index-ui-highest};
.thread-chat-container {
background: var(--background-color);
backdrop-filter: blur(14px);
border-radius: #{$border-radius-extra-large};
width: 20rem;
.header-wrapper {
padding: 12px;
@include flex-space-between;
.header-options {
@include flex-center;
position: relative;
.options-list {
position: absolute;
bottom: 0;
right: 0;
transform: translate(-24px, 100%);
background: var(--background-color);
padding: 8px 4px;
border-radius: #{$border-radius-medium};
backdrop-filter: blur(10px);
z-index: 100;
.options {
text-wrap: nowrap;
padding: 2px 4px;
border-radius: #{$border-radius-medium};
cursor: pointer;
&:hover {
color: var(--text-button-color);
background: var(--background-color-accent);
}
&.delete {
&:hover {
color: var(--log-error-text-color);
background: var(--log-error-background-color);
}
}
}
}
.options-button,
.close-button {
@include flex-center;
height: 24px;
width: 24px;
border-radius: #{$border-radius-medium};
&:hover {
background: var(--background-color-solid);
}
}
.close-button {
svg {
scale: 1.4;
}
}
}
}
.messages-wrapper {
padding: 12px;
padding-top: 0;
.edit-container {
.input-container {
textarea{
background: var(--background-color);
&:focus{
outline-color: var(--border-color-accent);
}
}
}
.actions-container {
@include flex-space-between;
width: 100%;
margin: 8px 0;
.actions {
@include flex-center;
gap: 4px;
.cancel-button,
.save-button {
padding: 4px 10px;
border-radius: #{$border-radius-large};
background: var(--background-color-solid);
outline: 1px solid var(--border-color);
}
.save-button {
color: var(--text-button-color);
background: var(--background-color-accent);
outline: none;
}
}
}
}
.message-container {
position: relative;
@include flex-space-between;
align-items: flex-start;
gap: 12px;
margin-top: 8px;
&:first-child{
margin: 0;
}
.profile {
height: 28px;
width: 28px;
min-width: 28px;
text-transform: uppercase;
line-height: 28px;
text-align: center;
border-radius: #{$border-radius-circle};
}
.content {
width: 100%;
.user-details {
display: flex;
align-items: flex-end;
gap: 8px;
.user-name {
text-transform: capitalize;
}
.time {
font-size: var(--font-size-tiny);
color: var(--input-text-color);
}
}
}
.more-options {
position: absolute;
right: 0;
top: 0;
.more-options-button {
@include flex-center;
height: 18px;
width: 18px;
border-radius: #{$border-radius-small};
&:hover{
background: var(--background-color-solid);
}
}
.options-list {
position: absolute;
right: 0;
padding: 3px 6px;
background: var(--background-color);
backdrop-filter: blur(10px);
outline: 1px solid var(--border-color);
border-radius: #{$border-radius-medium};
z-index: 100;
.option {
width: 100%;
border-radius: #{$border-radius-medium};
padding: 2px 6px;
text-align: start;
&:hover{
background: var(--background-color-accent);
color: var(--text-button-color);
}
}
}
}
.message{
margin-top: 6px;
}
}
}
.send-message-wrapper {
padding: 12px;
padding-top: 8px;
.input-container {
position: relative;
@include flex-space-between;
background: var(--background-color);
border-radius: #{$border-radius-extra-large};
outline: 1px solid var(--border-color);
textarea {
background: transparent;
outline: none;
width: calc(100% - 36px);
overflow: hidden;
line-height: 28px;
max-height: 108px;
}
.sent-button {
position: absolute;
right: 2px;
bottom: 2px;
@include flex-center;
padding: 2px;
svg {
rotate: 45deg;
}
}
.disable-send-btn {
filter: saturate(0);
}
&.active {
background: var(--background-color-solid);
padding-top: 4px;
flex-direction: column;
align-items: end;
textarea {
width: 100%;
line-height: 18px;
}
.sent-button {
position: relative;
}
}
}
}
}
}

21
app/src/types/collaborationTypes.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
interface CommentSchema {
state: "active" | "inactive";
commentId: string;
creatorId: string;
createdAt: string;
comment: string;
lastUpdatedAt: string;
position: [number, number, number];
rotation: [number, number, number];
replies: Reply[];
}
interface Reply {
replyId: string;
creatorId: string;
createdAt: string;
lastUpdatedAt: string;
reply: string;
}
type CommentsSchema = CommentSchema[];