diff --git a/app/package-lock.json b/app/package-lock.json index d63595f..1b6b2fd 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -31,7 +31,7 @@ "chartjs-plugin-annotation": "^3.1.0", "dxf-parser": "^1.1.2", "glob": "^11.0.0", - "gsap": "^3.12.5", + "gsap": "^3.13.0", "html2canvas": "^1.4.1", "immer": "^9.0.21", "leva": "^0.10.0", @@ -2026,7 +2026,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -2038,7 +2038,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -4180,6 +4180,26 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -4291,25 +4311,25 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true + "devOptional": true }, "node_modules/@turf/along": { "version": "7.2.0", @@ -9063,7 +9083,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "node_modules/cross-env": { "version": "7.0.3", @@ -9940,7 +9960,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -12291,9 +12311,10 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/gsap": { - "version": "3.12.5", - "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz", - "integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==" + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.13.0.tgz", + "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, "node_modules/gzip-size": { "version": "6.0.0", @@ -15324,7 +15345,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -20801,7 +20822,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -20844,7 +20865,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "dependencies": { "acorn": "^8.11.0" }, @@ -20856,7 +20877,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -21352,7 +21373,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "8.1.1", @@ -22411,7 +22432,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } diff --git a/app/package.json b/app/package.json index ede2f49..9558bd3 100644 --- a/app/package.json +++ b/app/package.json @@ -26,7 +26,7 @@ "chartjs-plugin-annotation": "^3.1.0", "dxf-parser": "^1.1.2", "glob": "^11.0.0", - "gsap": "^3.12.5", + "gsap": "^3.13.0", "html2canvas": "^1.4.1", "immer": "^9.0.21", "leva": "^0.10.0", diff --git a/app/src/modules/visualization/widgets/2d/DraggableWidget.tsx b/app/src/modules/visualization/widgets/2d/DraggableWidget.tsx index 7642aa8..11e0cfd 100644 --- a/app/src/modules/visualization/widgets/2d/DraggableWidget.tsx +++ b/app/src/modules/visualization/widgets/2d/DraggableWidget.tsx @@ -1,5 +1,10 @@ +import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; +import { gsap } from "gsap"; +import { Draggable } from "gsap/Draggable"; + import { useWidgetStore } from "../../../../store/useWidgetStore"; -import ProgressCard from "../2d/charts/ProgressCard"; +import { usePlayButtonStore } from "../../../../store/usePlayButtonStore"; +import { useSelectedZoneStore } from "../../../../store/visualization/useZoneStore"; import PieGraphComponent from "../2d/charts/PieGraphComponent"; import BarGraphComponent from "../2d/charts/BarGraphComponent"; import LineGraphComponent from "../2d/charts/LineGraphComponent"; @@ -12,13 +17,11 @@ import { DublicateIcon, KebabIcon, } from "../../../../components/icons/ExportCommonIcons"; -import { useEffect, useRef, useState } from "react"; -import { useClickOutside } from "../../functions/handleWidgetsOuterClick"; -import { useSocketStore } from "../../../../store/builder/store"; -import { usePlayButtonStore } from "../../../../store/usePlayButtonStore"; -import OuterClick from "../../../../utils/outerClick"; -import useChartStore from "../../../../store/visualization/useChartStore"; import { useParams } from "react-router-dom"; +import { useSocketStore } from "../../../../store/builder/store"; +import useChartStore from "../../../../store/visualization/useChartStore"; + +gsap.registerPlugin(Draggable); type Side = "top" | "bottom" | "left" | "right"; @@ -30,73 +33,73 @@ interface Widget { data: any; } -export const DraggableWidget = ({ - widget, - hiddenPanels, - index, - onReorder, - openKebabId, - setOpenKebabId, - selectedZone, - setSelectedZone, -}: { - selectedZone: { - zoneName: string; - zoneUuid: string; - activeSides: Side[]; - points: []; - panelOrder: Side[]; - lockedPanels: Side[]; - widgets: Widget[]; - }; - setSelectedZone: React.Dispatch< - React.SetStateAction<{ - zoneName: string; - activeSides: Side[]; - panelOrder: Side[]; - points: []; - lockedPanels: Side[]; - zoneUuid: string; - zoneViewPortTarget: number[]; - zoneViewPortPosition: number[]; - widgets: { - id: string; - type: string; - title: string; - panel: Side; - data: any; - }[]; - }> - >; - +type DraggableWidgetProps = { widget: any; - hiddenPanels: string[]; - index: number; - onReorder: (fromIndex: number, toIndex: number) => void; + index: any openKebabId: string | null; setOpenKebabId: (id: string | null) => void; + +}; + +export const DraggableWidget = forwardRef(({ + widget, + index, + openKebabId, + setOpenKebabId }) => { - const { visualizationSocket } = useSocketStore(); - const { selectedChartId, setSelectedChartId } = useWidgetStore(); + + const { selectedZone, setSelectedZone } = useSelectedZoneStore(); + const widgetRef = useRef(null); const [panelDimensions, setPanelDimensions] = useState<{ [side in Side]?: { width: number; height: number }; }>({}); - const { measurements, duration, name } = useChartStore(); - const { isPlaying } = usePlayButtonStore(); - const [canvasDimensions, setCanvasDimensions] = useState({ width: 0, height: 0, }); const { projectId } = useParams(); - useEffect(() => {}, [measurements, duration, name]); - const handlePointerDown = () => { - if (selectedChartId?.id !== widget.id) { - setSelectedChartId(widget); + const { visualizationSocket } = useSocketStore(); + const { selectedChartId, setSelectedChartId } = useWidgetStore(); + const { measurements, duration, name } = useChartStore(); + + + + const handleKebabClick = (event: React.MouseEvent) => { + event.stopPropagation(); + if (openKebabId === widget.id) { + setOpenKebabId(null); + } else { + setOpenKebabId(widget.id); } }; - const chartWidget = useRef(null); + const isPanelFull = (panel: Side) => { + const currentWidgetCount = getCurrentWidgetCount(panel); + const panelCapacity = calculatePanelCapacity(panel); + + return currentWidgetCount > panelCapacity; + }; + const panelSize = Math.max( + Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25), + 170 // Min 170px + ); + const getCurrentWidgetCount = (panel: Side) => + selectedZone.widgets.filter((w) => w.panel === panel).length; + // Calculate panel capacity + + const calculatePanelCapacity = (panel: Side) => { + const CHART_WIDTH = panelSize; + const CHART_HEIGHT = panelSize; + + const dimensions = panelDimensions[panel]; + if (!dimensions) { + return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities + } + + return panel === "top" || panel === "bottom" + ? Math.max(1, Math.floor(dimensions.width / CHART_WIDTH)) + : Math.max(1, Math.floor(dimensions.height / CHART_HEIGHT)); + }; const deleteSelectedChart = async () => { try { @@ -107,8 +110,9 @@ export const DraggableWidget = ({ zoneUuid: selectedZone.zoneUuid, widgetID: widget.id, organization: organization, - projectId,userId + projectId, userId }; + console.log('deleteWidget: ', deleteWidget); if (visualizationSocket) { setSelectedChartId(null); @@ -139,38 +143,6 @@ export const DraggableWidget = ({ setOpenKebabId(null); } }; - - // Calculate panel size - const panelSize = Math.max( - Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25), - 170 // Min 170px - ); - - const getCurrentWidgetCount = (panel: Side) => - selectedZone.widgets.filter((w) => w.panel === panel).length; - // Calculate panel capacity - - const calculatePanelCapacity = (panel: Side) => { - const CHART_WIDTH = panelSize; - const CHART_HEIGHT = panelSize; - - const dimensions = panelDimensions[panel]; - if (!dimensions) { - return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities - } - - return panel === "top" || panel === "bottom" - ? Math.max(1, Math.floor(dimensions.width / CHART_WIDTH)) - : Math.max(1, Math.floor(dimensions.height / CHART_HEIGHT)); - }; - - const isPanelFull = (panel: Side) => { - const currentWidgetCount = getCurrentWidgetCount(panel); - const panelCapacity = calculatePanelCapacity(panel); - - return currentWidgetCount > panelCapacity; - }; - const duplicateWidget = async () => { try { const email = localStorage.getItem("email") || ""; @@ -191,7 +163,7 @@ export const DraggableWidget = ({ organization: organization, zoneUuid: selectedZone.zoneUuid, widget: duplicatedWidget, - projectId,userId + projectId, userId }; if (visualizationSocket) { @@ -217,195 +189,117 @@ export const DraggableWidget = ({ } }; - const handleKebabClick = (event: React.MouseEvent) => { - event.stopPropagation(); - if (openKebabId === widget.id) { - setOpenKebabId(null); - } else { - setOpenKebabId(widget.id); + const handlePointerDown = () => { + if (selectedChartId?.id !== widget.id) { + setSelectedChartId(widget); } }; - - const widgetRef = useRef(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - widgetRef.current && - !widgetRef.current.contains(event.target as Node) - ) { - setOpenKebabId(null); - } - }; - - document.addEventListener("mousedown", handleClickOutside); - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [setOpenKebabId]); - - const handleDragStart = (event: React.DragEvent) => { - event.dataTransfer.setData("text/plain", index.toString()); // Store the index of the dragged widget - }; - const handleDragEnter = (event: React.DragEvent) => { - event.preventDefault(); // Allow drop - }; - - const handleDragOver = (event: React.DragEvent) => { - event.preventDefault(); // Allow drop - }; - - const handleDrop = (event: React.DragEvent) => { - event.preventDefault(); - const fromIndex = parseInt(event.dataTransfer.getData("text/plain"), 10); // Get the dragged widget's index - const toIndex = index; // The index of the widget where the drop occurred - if (fromIndex !== toIndex) { - onReorder(fromIndex, toIndex); // Call the reorder function passed as a prop - } - }; - - // useClickOutside(chartWidget, () => { - // setSelectedChartId(null); - // }); - - // Track canvas dimensions - - // Current: Two identical useEffect hooks for canvas dimensions - // Remove the duplicate and keep only one - useEffect(() => { - const canvas = document.getElementById("real-time-vis-canvas"); - if (!canvas) return; - - const updateCanvasDimensions = () => { - const rect = canvas.getBoundingClientRect(); - setCanvasDimensions({ - width: rect.width, - height: rect.height, - }); - }; - - updateCanvasDimensions(); - const resizeObserver = new ResizeObserver(updateCanvasDimensions); - resizeObserver.observe(canvas); - - return () => resizeObserver.unobserve(canvas); - }, []); + const { isPlaying } = usePlayButtonStore(); return ( - <> -
{ - setSelectedChartId(widget); - }} - > - {/* Kebab Icon */} -
- -
+ onClick={() => { + setSelectedChartId(widget); + }} + onPointerDown={handlePointerDown} - {/* Kebab Options */} - {openKebabId === widget.id && ( -
-
-
- -
-
Duplicate
-
-
{ - e.stopPropagation(); - deleteSelectedChart(); - }} - > -
- -
-
Delete
-
-
- )} - - {/* Render charts based on widget type */} - - {widget.type === "progress 1" && ( - - )} - {widget.type === "progress 2" && ( - - )} - {widget.type === "line" && ( - - )} - {widget.type === "bar" && ( - - )} - {widget.type === "pie" && ( - - )} - {widget.type === "doughnut" && ( - - )} - {widget.type === "polarArea" && ( - - )} + > + {index + 1} +
+
- + {openKebabId === widget.id && ( +
+
+
+ +
+
Duplicate
+
+
{ + e.stopPropagation(); + deleteSelectedChart(); + }} + > +
+ +
+
Delete
+
+
+ )} + + {/* Render charts based on widget type */} + + {widget.type === "progress 1" && ( + + )} + + {widget.type === "progress 2" && ( + + )} + + {widget.type === "line" && ( + + )} + + {widget.type === "bar" && ( + + )} + + {widget.type === "pie" && ( + + )} + + {widget.type === "doughnut" && ( + + )} + + {widget.type === "polarArea" && ( + + )} +
); -}; +} +); + diff --git a/app/src/modules/visualization/widgets/3d/Dropped3dWidget.tsx b/app/src/modules/visualization/widgets/3d/Dropped3dWidget.tsx index 56ec30c..d101df1 100644 --- a/app/src/modules/visualization/widgets/3d/Dropped3dWidget.tsx +++ b/app/src/modules/visualization/widgets/3d/Dropped3dWidget.tsx @@ -46,17 +46,8 @@ export default function Dropped3dWidgets() { const { top, setTop } = useTopData(); const { left, setLeft } = useLeftData(); const { rightSelect, setRightSelect } = useRightSelected(); - const { editWidgetOptions, setEditWidgetOptions } = - useEditWidgetOptionsStore(); - const { - zoneWidgetData, - setZoneWidgetData, - addWidget, - updateWidgetPosition, - updateWidgetRotation, - tempWidget, - tempWidgetPosition, - } = useZoneWidgetStore(); + const { editWidgetOptions, setEditWidgetOptions } =useEditWidgetOptionsStore(); + const {zoneWidgetData,setZoneWidgetData,addWidget,updateWidgetPosition,updateWidgetRotation,tempWidget,tempWidgetPosition,} = useZoneWidgetStore(); const { setWidgets3D } = use3DWidget(); const { visualizationSocket } = useSocketStore(); const { rightClickSelected, setRightClickSelected } = useRightClickSelected(); @@ -67,12 +58,8 @@ export default function Dropped3dWidgets() { const mouseStartRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); const { setSelectedChartId } = useWidgetStore(); const { measurements, duration } = useChartStore(); - let [floorPlanesVertical, setFloorPlanesVertical] = useState( - new THREE.Plane(new THREE.Vector3(0, 1, 0)) - ); - const [intersectcontextmenu, setintersectcontextmenu] = useState< - number | undefined - >(); + let [floorPlanesVertical, setFloorPlanesVertical] = useState(new THREE.Plane(new THREE.Vector3(0, 1, 0))); + const [intersectcontextmenu, setintersectcontextmenu] = useState(); const [horizontalX, setHorizontalX] = useState(); const [horizontalZ, setHorizontalZ] = useState(); @@ -87,12 +74,7 @@ export default function Dropped3dWidgets() { const organization = email?.split("@")[1]?.split(".")[0]; async function get3dWidgetData() { - const result = await get3dWidgetZoneData( - selectedZone.zoneUuid, - organization, - projectId - ); - + const result = await get3dWidgetZoneData(selectedZone.zoneUuid,organization, projectId); setWidgets3D(result); if (result.length < 0) return; diff --git a/app/src/modules/visualization/widgets/panel/Panel.tsx b/app/src/modules/visualization/widgets/panel/Panel.tsx index c9c62c9..544049c 100644 --- a/app/src/modules/visualization/widgets/panel/Panel.tsx +++ b/app/src/modules/visualization/widgets/panel/Panel.tsx @@ -1,13 +1,24 @@ -import React, { useEffect, useMemo, useRef, useState } from "react"; - -import { arrayMove } from "@dnd-kit/sortable"; -import { useSocketStore } from "../../../../store/builder/store"; -import { usePlayButtonStore } from "../../../../store/usePlayButtonStore"; -import { useWidgetStore } from "../../../../store/useWidgetStore"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import gsap from "gsap"; +import { Draggable } from "gsap/Draggable"; import { DraggableWidget } from "../2d/DraggableWidget"; +import { useWidgetStore } from "../../../../store/useWidgetStore"; +import { generateUniqueId } from "../../../../functions/generateUniqueId"; +import { useSocketStore } from "../../../../store/builder/store"; import { useParams } from "react-router-dom"; +import { arrayMove } from "@dnd-kit/sortable"; +import { clamp } from "three/src/math/MathUtils"; + +gsap.registerPlugin(Draggable); type Side = "top" | "bottom" | "left" | "right"; +const GAP = 6; interface Widget { id: string; @@ -29,48 +40,90 @@ interface PanelProps { zoneViewPortPosition: number[]; widgets: Widget[]; }; - setSelectedZone: React.Dispatch< - React.SetStateAction<{ - zoneName: string; - activeSides: Side[]; - panelOrder: Side[]; - lockedPanels: Side[]; - points: []; - zoneUuid: string; - zoneViewPortTarget: number[]; - zoneViewPortPosition: number[]; - widgets: Widget[]; - }> - >; + setSelectedZone: React.Dispatch>; hiddenPanels: any; setZonesData: React.Dispatch>; waitingPanels: any; } -const generateUniqueId = () => - `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; +type SortableItem = { + dragger: Draggable; + element: HTMLDivElement; + index: number; + setIndex: (idx: number) => void; +}; const Panel: React.FC = ({ selectedZone, setSelectedZone, hiddenPanels, - setZonesData, - waitingPanels, }) => { - const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({}); - const [panelDimensions, setPanelDimensions] = useState<{ - [side in Side]?: { width: number; height: number }; - }>({}); + // const rowSize = 160; + // const colSize = 160; + const [rowSize, setRowSize] = useState(160); + const [colSize, setColSize] = useState(160); const [openKebabId, setOpenKebabId] = useState(null); - const { isPlaying } = usePlayButtonStore(); + const [initialItems, setInitialItems] = useState([]); + + const containerRef = useRef<{ [side in Side]?: HTMLDivElement }>({}); + const itemRefs = useRef<{ [side in Side]?: HTMLDivElement[] }>({}); + const sortables = useRef<{ [side in Side]?: SortableItem[] }>({}); + const draggables = useRef<{ [side in Side]?: Draggable[] }>({}); + const { visualizationSocket } = useSocketStore(); const [canvasDimensions, setCanvasDimensions] = useState({ width: 0, height: 0, }); + const [panelDimensions, setPanelDimensions] = useState<{ + [side in Side]?: { width: number; height: number }; + }>({}); const { projectId } = useParams(); + const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({}); + // function handleResize() { + // const canvas = document.getElementById("sceneCanvas"); + // if (canvas) { + // const rect = canvas.getBoundingClientRect(); + // setRowSize(rect.height / 6) + // setColSize(rect.width / 6) + // } + // } + + // useEffect(() => { + // window.addEventListener("resize", handleResize); + // return () => { + // window.removeEventListener("resize", handleResize); + // }; + // }, []); + + useEffect(() => { + const observers: ResizeObserver[] = []; + const currentPanelRefs = panelRefs.current; + selectedZone.activeSides.forEach((side) => { + const element = currentPanelRefs[side]; + if (element) { + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const { width, height } = entry.contentRect; + setPanelDimensions((prev) => ({ + ...prev, + [side]: { width, height }, + })); + } + }); + + observer.observe(element); + observers.push(observer); + } + }); + + return () => { + observers.forEach((observer) => observer.disconnect()); + }; + }, [selectedZone.activeSides]); // Track canvas dimensions + useEffect(() => { const canvas = document.getElementById("real-time-vis-canvas"); if (!canvas) return; @@ -95,8 +148,10 @@ const Panel: React.FC = ({ Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25), 170 // Min 170px ); + useEffect(() => { + setInitialItems(selectedZone.widgets); + }, [selectedZone]); - // Define getPanelStyle const getPanelStyle = useMemo( () => (side: Side) => { const currentIndex = selectedZone.panelOrder.indexOf(side); @@ -137,27 +192,139 @@ const Panel: React.FC = ({ }, [selectedZone.panelOrder, panelSize] ); + const handleReorder = (fromIndex: number, toIndex: number, panel: Side) => { + setSelectedZone((prev: any) => { + const widgetsInPanel = prev.widgets.filter((w: any) => w.panel === panel); + const reorderedWidgets = arrayMove(widgetsInPanel, fromIndex, toIndex); + const updatedWidgets = prev.widgets + .filter((w: any) => w.panel !== panel) + .concat(reorderedWidgets); - // Handle drop event - const handleDrop = (e: React.DragEvent, panel: Side) => { - e.preventDefault(); - const { draggedAsset } = useWidgetStore.getState(); - if ( - !draggedAsset || - isPanelLocked(panel) || - hiddenPanels[selectedZone.zoneUuid]?.includes(panel) - ) - return; - - const currentWidgetsCount = getCurrentWidgetCount(panel); - const maxCapacity = calculatePanelCapacity(panel); - - if (currentWidgetsCount < maxCapacity) { - addWidgetToPanel(draggedAsset, panel); - } + return { + ...prev, + widgets: updatedWidgets, + }; + }); + setInitialItems(selectedZone.widgets); }; - // Check if panel is locked + useEffect(() => { }, [initialItems]); + + let fromIndex = useRef(); + let toIndex = useRef(); + let itemsRef = useRef(); + + useEffect(() => { + selectedZone.activeSides.forEach((side) => { + const container = containerRef.current[side]; + const widgets = initialItems.filter((w) => w.panel === side); + const itemList = itemRefs.current[side] || []; + + if (!container || widgets.length === 0) return; + + gsap.to(container, { autoAlpha: 1, duration: 0.5 }); + + // Cleanup old draggables + draggables.current[side]?.forEach((d) => d.kill()); + draggables.current[side] = []; + sortables.current[side] = []; + + widgets.forEach((widget, index) => { + const element = itemList[index]; + if (!element) return; + + const content = element.querySelector(".item-content") as HTMLElement; + const order = element.querySelector(".order") as HTMLElement; + + const animation = gsap.to(content, { + boxShadow: "rgba(0,0,0,0.2) 0px 16px 32px 0px", + scale: 1.05, + paused: true, + duration: 0.25, + }); + + const sortable: SortableItem = { + dragger: null as any, + element, + index, + setIndex(idx: number) { + sortable.index = idx; + if (order) order.textContent = (idx + 1).toString(); + }, + }; + + const isHorizontal = side === "top" || side === "bottom"; + + const dragger = Draggable.create(element, { + type: isHorizontal ? "x" : "y", + cursor: "grab", + onPress() { + animation.play(); + this.update(); + }, + onDrag() { + const pos = isHorizontal ? this.x : this.y; + const newIndex = clamp( + Math.round(pos / rowSize), + 0, + widgets.length - 1 + ); + if (newIndex !== sortable.index) { + const arr = sortables.current[side]!; + const movedArr = arrayMove(arr, sortable.index, newIndex); + + sortables.current[side] = movedArr; + if (newIndex === movedArr.length - 1) { + container.appendChild(element); + } else { + const i = sortable.index > newIndex ? newIndex : newIndex + 1; + container.insertBefore(element, container.children[i]); + } + + movedArr.forEach((s, idx) => { + s.setIndex(idx); + if (s.element !== element) { + gsap.to(s.element, { + [isHorizontal ? "x" : "y"]: idx * (isHorizontal ? colSize : rowSize + GAP), + duration: 0.25, + overwrite: true, + }); + } + }); + } + }, + onRelease() { + animation.reverse(); + handleReorder(index, sortable.index, side); + + gsap.to(element, { + [isHorizontal ? "x" : "y"]: sortable.index * (isHorizontal ? colSize : rowSize + GAP), + duration: 0.3, + }); + + }, + })[0]; + + sortable.dragger = dragger; + draggables.current[side]?.push(dragger); + sortables.current[side]?.push(sortable); + + gsap.set(element, { + [isHorizontal ? "x" : "y"]: index * (isHorizontal ? colSize : rowSize + GAP), + }); + + }); + }); + + return () => { + Object.values(draggables.current).forEach((draggers) => { + draggers?.forEach((d) => d.kill()); + }); + draggables.current = {}; + sortables.current = {}; + }; + }, [initialItems]); + const isPanelLocked = (panel: Side) => selectedZone.lockedPanels.includes(panel); @@ -171,6 +338,7 @@ const Panel: React.FC = ({ const CHART_HEIGHT = panelSize - 10; const dimensions = panelDimensions[panel]; + if (!dimensions) { return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities } @@ -196,65 +364,40 @@ const Panel: React.FC = ({ organization: organization, zoneUuid: selectedZone.zoneUuid, widget: newWidget, - projectId, userId + projectId, + userId, }; if (visualizationSocket) { visualizationSocket.emit("v1:viz-widget:add", addWidget); } - setSelectedZone((prev) => ({ + setSelectedZone((prev: any) => ({ ...prev, widgets: [...prev.widgets, newWidget], })); }; - // Observe panel dimensions - useEffect(() => { - const observers: ResizeObserver[] = []; - const currentPanelRefs = panelRefs.current; + // Handle drop event + const handleDrop = (e: React.DragEvent, panel: Side) => { + e.preventDefault(); - selectedZone.activeSides.forEach((side) => { - const element = currentPanelRefs[side]; - if (element) { - const observer = new ResizeObserver((entries) => { - for (const entry of entries) { - const { width, height } = entry.contentRect; - setPanelDimensions((prev) => ({ - ...prev, - [side]: { width, height }, - })); - } - }); + const { draggedAsset } = useWidgetStore.getState(); + if ( + !draggedAsset || + isPanelLocked(panel) || + hiddenPanels[selectedZone.zoneUuid]?.includes(panel) + ) + return; - observer.observe(element); - observers.push(observer); - } - }); + const currentWidgetsCount = getCurrentWidgetCount(panel); + const maxCapacity = calculatePanelCapacity(panel); - return () => { - observers.forEach((observer) => observer.disconnect()); - }; - }, [selectedZone.activeSides]); - - // Handle widget reordering - const handleReorder = (fromIndex: number, toIndex: number, panel: Side) => { - setSelectedZone((prev) => { - const widgetsInPanel = prev.widgets.filter((w) => w.panel === panel); - const reorderedWidgets = arrayMove(widgetsInPanel, fromIndex, toIndex); - - const updatedWidgets = prev.widgets - .filter((widget) => widget.panel !== panel) - .concat(reorderedWidgets); - - return { - ...prev, - widgets: updatedWidgets, - }; - }); + if (currentWidgetsCount < maxCapacity) { + addWidgetToPanel(draggedAsset, panel); + } else { + } }; - - // Calculate capacities and dimensions const topWidth = getPanelStyle("top").width; const bottomWidth = getPanelStyle("bottom").height; const leftHeight = getPanelStyle("left").height; @@ -264,7 +407,6 @@ const Panel: React.FC = ({ const bottomCapacity = calculatePanelCapacity("bottom"); const leftCapacity = calculatePanelCapacity("left"); const rightCapacity = calculatePanelCapacity("right"); - return ( <>