v2-ui #94
app/src
components
icons
layout
templates
ui
collaboration
compareVersion
inputs
list
log
menu
modules
builder/geomentries/lines
collaboration
scene
simulation/events/arrows
visualization/widgets
pages
store
styles
types
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -13,7 +13,7 @@ const Visualization = () => {
|
|||
return (
|
||||
<div className="visualization-right-sideBar">
|
||||
<ToggleHeader
|
||||
options={["Data"]}
|
||||
options={["Data","Design"]}
|
||||
activeOption={activeOption}
|
||||
handleClick={handleToggleClick}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
|
@ -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;
|
|
@ -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
|
|
@ -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} />
|
||||
</>
|
||||
|
|
|
@ -218,7 +218,7 @@ const MeasurementTool = () => {
|
|||
sprite
|
||||
>
|
||||
<div>
|
||||
{startConePosition.distanceTo(endConePosition).toFixed(2)} m
|
||||
{(startConePosition.distanceTo(endConePosition) + (coneSize.height)).toFixed(2)} m
|
||||
</div>
|
||||
</Html>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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 })),
|
||||
|
|
|
@ -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 }),
|
||||
}));
|
|
@ -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);
|
||||
},
|
||||
}))
|
||||
);
|
|
@ -3,7 +3,8 @@
|
|||
|
||||
// global input style
|
||||
|
||||
input {
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
border-radius: #{$border-radius-large};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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[];
|
Loading…
Reference in New Issue