Merge remote-tracking branch 'origin/v3-refactor' into v3

This commit is contained in:
Jerald-Golden-B 2025-06-21 15:02:52 +05:30
commit 8cf12c75df
6 changed files with 672 additions and 452 deletions

57
app/package-lock.json generated
View File

@ -31,7 +31,7 @@
"chartjs-plugin-annotation": "^3.1.0", "chartjs-plugin-annotation": "^3.1.0",
"dxf-parser": "^1.1.2", "dxf-parser": "^1.1.2",
"glob": "^11.0.0", "glob": "^11.0.0",
"gsap": "^3.12.5", "gsap": "^3.13.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"immer": "^9.0.21", "immer": "^9.0.21",
"leva": "^0.10.0", "leva": "^0.10.0",
@ -2026,7 +2026,7 @@
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "0.3.9" "@jridgewell/trace-mapping": "0.3.9"
}, },
@ -2038,7 +2038,7 @@
"version": "0.3.9", "version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10" "@jridgewell/sourcemap-codec": "^1.4.10"
@ -4180,6 +4180,26 @@
"url": "https://github.com/sponsors/gregberge" "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": { "node_modules/@testing-library/jest-dom": {
"version": "5.17.0", "version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
@ -4291,25 +4311,25 @@
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"dev": true "devOptional": true
}, },
"node_modules/@tsconfig/node12": { "node_modules/@tsconfig/node12": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true "devOptional": true
}, },
"node_modules/@tsconfig/node14": { "node_modules/@tsconfig/node14": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true "devOptional": true
}, },
"node_modules/@tsconfig/node16": { "node_modules/@tsconfig/node16": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true "devOptional": true
}, },
"node_modules/@turf/along": { "node_modules/@turf/along": {
"version": "7.2.0", "version": "7.2.0",
@ -9063,7 +9083,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true "devOptional": true
}, },
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "7.0.3", "version": "7.0.3",
@ -9940,7 +9960,7 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true, "devOptional": true,
"engines": { "engines": {
"node": ">=0.3.1" "node": ">=0.3.1"
} }
@ -12291,9 +12311,10 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
}, },
"node_modules/gsap": { "node_modules/gsap": {
"version": "3.12.5", "version": "3.13.0",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.13.0.tgz",
"integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==" "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
}, },
"node_modules/gzip-size": { "node_modules/gzip-size": {
"version": "6.0.0", "version": "6.0.0",
@ -15324,7 +15345,7 @@
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true "devOptional": true
}, },
"node_modules/makeerror": { "node_modules/makeerror": {
"version": "1.0.12", "version": "1.0.12",
@ -20801,7 +20822,7 @@
"version": "10.9.2", "version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@cspotcode/source-map-support": "^0.8.0", "@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7", "@tsconfig/node10": "^1.0.7",
@ -20844,7 +20865,7 @@
"version": "8.3.4", "version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"acorn": "^8.11.0" "acorn": "^8.11.0"
}, },
@ -20856,7 +20877,7 @@
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true "devOptional": true
}, },
"node_modules/tsconfig-paths": { "node_modules/tsconfig-paths": {
"version": "3.15.0", "version": "3.15.0",
@ -21352,7 +21373,7 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true "devOptional": true
}, },
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
"version": "8.1.1", "version": "8.1.1",
@ -22411,7 +22432,7 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true, "devOptional": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }

View File

@ -26,7 +26,7 @@
"chartjs-plugin-annotation": "^3.1.0", "chartjs-plugin-annotation": "^3.1.0",
"dxf-parser": "^1.1.2", "dxf-parser": "^1.1.2",
"glob": "^11.0.0", "glob": "^11.0.0",
"gsap": "^3.12.5", "gsap": "^3.13.0",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"immer": "^9.0.21", "immer": "^9.0.21",
"leva": "^0.10.0", "leva": "^0.10.0",

View File

@ -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 { 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 PieGraphComponent from "../2d/charts/PieGraphComponent";
import BarGraphComponent from "../2d/charts/BarGraphComponent"; import BarGraphComponent from "../2d/charts/BarGraphComponent";
import LineGraphComponent from "../2d/charts/LineGraphComponent"; import LineGraphComponent from "../2d/charts/LineGraphComponent";
@ -12,13 +17,11 @@ import {
DublicateIcon, DublicateIcon,
KebabIcon, KebabIcon,
} from "../../../../components/icons/ExportCommonIcons"; } 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 { useParams } from "react-router-dom";
import { useSocketStore } from "../../../../store/builder/store";
import useChartStore from "../../../../store/visualization/useChartStore";
gsap.registerPlugin(Draggable);
import { getUserData } from "../../../../functions/getUserData"; import { getUserData } from "../../../../functions/getUserData";
type Side = "top" | "bottom" | "left" | "right"; type Side = "top" | "bottom" | "left" | "right";
@ -31,73 +34,74 @@ interface Widget {
data: any; data: any;
} }
export const DraggableWidget = ({ type DraggableWidgetProps = {
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;
}[];
}>
>;
widget: any; widget: any;
hiddenPanels: string[]; index: any
index: number;
onReorder: (fromIndex: number, toIndex: number) => void;
openKebabId: string | null; openKebabId: string | null;
setOpenKebabId: (id: string | null) => void; setOpenKebabId: (id: string | null) => void;
};
export const DraggableWidget = forwardRef<HTMLDivElement, DraggableWidgetProps>(({
widget,
index,
openKebabId,
setOpenKebabId
}) => { }) => {
const { visualizationSocket } = useSocketStore();
const { selectedChartId, setSelectedChartId } = useWidgetStore(); const { selectedZone, setSelectedZone } = useSelectedZoneStore();
const widgetRef = useRef<HTMLDivElement | null>(null);
const [panelDimensions, setPanelDimensions] = useState<{ const [panelDimensions, setPanelDimensions] = useState<{
[side in Side]?: { width: number; height: number }; [side in Side]?: { width: number; height: number };
}>({}); }>({});
const { measurements, duration, name } = useChartStore();
const { isPlaying } = usePlayButtonStore();
const { userId, organization, email } = getUserData();
const [canvasDimensions, setCanvasDimensions] = useState({ const [canvasDimensions, setCanvasDimensions] = useState({
width: 0, width: 0,
height: 0, height: 0,
}); });
const { projectId } = useParams(); const { projectId } = useParams();
useEffect(() => { }, [measurements, duration, name]); const { visualizationSocket } = useSocketStore();
const handlePointerDown = () => { const { selectedChartId, setSelectedChartId } = useWidgetStore();
if (selectedChartId?.id !== widget.id) { const { measurements, duration, name } = useChartStore();
setSelectedChartId(widget); const { userId, organization, email } = getUserData();
const handleKebabClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
if (openKebabId === widget.id) {
setOpenKebabId(null);
} else {
setOpenKebabId(widget.id);
} }
}; };
const chartWidget = useRef<HTMLDivElement>(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 () => { const deleteSelectedChart = async () => {
try { try {
@ -105,9 +109,10 @@ export const DraggableWidget = ({
let deleteWidget = { let deleteWidget = {
zoneUuid: selectedZone.zoneUuid, zoneUuid: selectedZone.zoneUuid,
widgetID: widget.id, widgetID: widget.id,
organization, organization: organization,
projectId, userId projectId, userId
}; };
console.log('deleteWidget: ', deleteWidget);
if (visualizationSocket) { if (visualizationSocket) {
setSelectedChartId(null); setSelectedChartId(null);
@ -138,38 +143,6 @@ export const DraggableWidget = ({
setOpenKebabId(null); 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 () => { const duplicateWidget = async () => {
try { try {
@ -214,193 +187,117 @@ export const DraggableWidget = ({
} }
}; };
const handleKebabClick = (event: React.MouseEvent<HTMLDivElement>) => { const handlePointerDown = () => {
event.stopPropagation(); if (selectedChartId?.id !== widget.id) {
if (openKebabId === widget.id) { setSelectedChartId(widget);
setOpenKebabId(null);
} else {
setOpenKebabId(widget.id);
} }
}; };
const { isPlaying } = usePlayButtonStore();
const widgetRef = useRef<HTMLDivElement | null>(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<HTMLDivElement>) => {
event.dataTransfer.setData("text/plain", index.toString()); // Store the index of the dragged widget
};
const handleDragEnter = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Allow drop
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault(); // Allow drop
};
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
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);
}, []);
return ( return (
<>
<div
draggable
key={widget.id}
className={`chart-container ${selectedChartId?.id === widget.id && !isPlaying && "activeChart"
}`}
onPointerDown={handlePointerDown}
onDragStart={handleDragStart}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{
width: ["top", "bottom"].includes(widget.panel)
? `calc(${canvasDimensions.width}px / 6)`
: undefined,
height: ["left", "right"].includes(widget.panel)
? `calc(${canvasDimensions.height - 15}px / 4)`
: undefined,
}}
ref={chartWidget}
onClick={() => {
setSelectedChartId(widget);
}}
>
{/* Kebab Icon */}
<div className="icon kebab" onClick={handleKebabClick}>
<KebabIcon />
</div>
{/* Kebab Options */} <div
{openKebabId === widget.id && (
<div className="kebab-options" ref={widgetRef}>
<div
className={`edit btn ${isPanelFull(widget.panel) ? "btn-blur" : ""
}`}
onClick={duplicateWidget}
>
<div className="icon">
<DublicateIcon />
</div>
<div className="label">Duplicate</div>
</div>
<div
className="edit btn"
onClick={(e) => {
e.stopPropagation();
deleteSelectedChart();
}}
>
<div className="icon">
<DeleteIcon />
</div>
<div className="label">Delete</div>
</div>
</div>
)}
{/* Render charts based on widget type */} className={`item-content ${selectedChartId?.id === widget.id && !isPlaying && "activeChart"
}`}
onClick={() => {
setSelectedChartId(widget);
}}
onPointerDown={handlePointerDown}
{widget.type === "progress 1" && ( >
<ProgressCard1 title={widget.title} id={widget.id} /> <span className="order"> {index + 1} </span>
)} <div className="icon kebab" onClick={handleKebabClick}>
{widget.type === "progress 2" && ( <KebabIcon />
<ProgressCard2 title={widget.title} id={widget.id} />
)}
{widget.type === "line" && (
<LineGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "bar" && (
<BarGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "pie" && (
<PieGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "doughnut" && (
<DoughnutGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "polarArea" && (
<PolarAreaGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
</div> </div>
</> {openKebabId === widget.id && (
<div className="kebab-options" ref={widgetRef}>
<div
className={`edit btn ${isPanelFull(widget.panel) ? "btn-blur" : ""
}`}
onClick={duplicateWidget}
>
<div className="icon">
<DublicateIcon />
</div>
<div className="label">Duplicate</div>
</div>
<div
className="edit btn"
onClick={(e) => {
e.stopPropagation();
deleteSelectedChart();
}}
>
<div className="icon">
<DeleteIcon />
</div>
<div className="label">Delete</div>
</div>
</div>
)}
{/* Render charts based on widget type */}
{widget.type === "progress 1" && (
<ProgressCard1 title={widget.title} id={widget.id} />
)}
{widget.type === "progress 2" && (
<ProgressCard2 title={widget.title} id={widget.id} />
)}
{widget.type === "line" && (
<LineGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "bar" && (
<BarGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "pie" && (
<PieGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "doughnut" && (
<DoughnutGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
{widget.type === "polarArea" && (
<PolarAreaGraphComponent
id={widget.id}
type={widget.type}
title={widget.title}
fontSize={widget.fontSize}
fontWeight={widget.fontWeight}
/>
)}
</div>
); );
}; }
);

View File

@ -47,17 +47,8 @@ export default function Dropped3dWidgets() {
const { top, setTop } = useTopData(); const { top, setTop } = useTopData();
const { left, setLeft } = useLeftData(); const { left, setLeft } = useLeftData();
const { rightSelect, setRightSelect } = useRightSelected(); const { rightSelect, setRightSelect } = useRightSelected();
const { editWidgetOptions, setEditWidgetOptions } = const { editWidgetOptions, setEditWidgetOptions } =useEditWidgetOptionsStore();
useEditWidgetOptionsStore(); const {zoneWidgetData,setZoneWidgetData,addWidget,updateWidgetPosition,updateWidgetRotation,tempWidget,tempWidgetPosition,} = useZoneWidgetStore();
const {
zoneWidgetData,
setZoneWidgetData,
addWidget,
updateWidgetPosition,
updateWidgetRotation,
tempWidget,
tempWidgetPosition,
} = useZoneWidgetStore();
const { setWidgets3D } = use3DWidget(); const { setWidgets3D } = use3DWidget();
const { visualizationSocket } = useSocketStore(); const { visualizationSocket } = useSocketStore();
const { rightClickSelected, setRightClickSelected } = useRightClickSelected(); const { rightClickSelected, setRightClickSelected } = useRightClickSelected();
@ -68,12 +59,8 @@ export default function Dropped3dWidgets() {
const mouseStartRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); const mouseStartRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const { setSelectedChartId } = useWidgetStore(); const { setSelectedChartId } = useWidgetStore();
const { measurements, duration } = useChartStore(); const { measurements, duration } = useChartStore();
let [floorPlanesVertical, setFloorPlanesVertical] = useState( let [floorPlanesVertical, setFloorPlanesVertical] = useState(new THREE.Plane(new THREE.Vector3(0, 1, 0)));
new THREE.Plane(new THREE.Vector3(0, 1, 0)) const [intersectcontextmenu, setintersectcontextmenu] = useState<number | undefined>();
);
const [intersectcontextmenu, setintersectcontextmenu] = useState<
number | undefined
>();
const [horizontalX, setHorizontalX] = useState<number | undefined>(); const [horizontalX, setHorizontalX] = useState<number | undefined>();
const [horizontalZ, setHorizontalZ] = useState<number | undefined>(); const [horizontalZ, setHorizontalZ] = useState<number | undefined>();
@ -86,12 +73,7 @@ export default function Dropped3dWidgets() {
async function get3dWidgetData() { async function get3dWidgetData() {
const result = await get3dWidgetZoneData( const result = await get3dWidgetZoneData(selectedZone.zoneUuid,organization, projectId);
selectedZone.zoneUuid,
organization,
projectId
);
setWidgets3D(result); setWidgets3D(result);
if (result.length < 0) return; if (result.length < 0) return;

View File

@ -1,14 +1,26 @@
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, {
useCallback,
import { arrayMove } from "@dnd-kit/sortable"; useEffect,
import { useSocketStore } from "../../../../store/builder/store"; useMemo,
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore"; useRef,
import { useWidgetStore } from "../../../../store/useWidgetStore"; useState,
} from "react";
import gsap from "gsap";
import { Draggable } from "gsap/Draggable";
import { DraggableWidget } from "../2d/DraggableWidget"; 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 { useParams } from "react-router-dom";
import { arrayMove } from "@dnd-kit/sortable";
import { clamp } from "three/src/math/MathUtils";
import { getUserData } from "../../../../functions/getUserData"; import { getUserData } from "../../../../functions/getUserData";
gsap.registerPlugin(Draggable);
type Side = "top" | "bottom" | "left" | "right"; type Side = "top" | "bottom" | "left" | "right";
const GAP = 6;
interface Widget { interface Widget {
id: string; id: string;
@ -30,50 +42,92 @@ interface PanelProps {
zoneViewPortPosition: number[]; zoneViewPortPosition: number[];
widgets: Widget[]; widgets: Widget[];
}; };
setSelectedZone: React.Dispatch< setSelectedZone: React.Dispatch<React.SetStateAction<any>>;
React.SetStateAction<{
zoneName: string;
activeSides: Side[];
panelOrder: Side[];
lockedPanels: Side[];
points: [];
zoneUuid: string;
zoneViewPortTarget: number[];
zoneViewPortPosition: number[];
widgets: Widget[];
}>
>;
hiddenPanels: any; hiddenPanels: any;
setZonesData: React.Dispatch<React.SetStateAction<any>>; setZonesData: React.Dispatch<React.SetStateAction<any>>;
waitingPanels: any; waitingPanels: any;
} }
const generateUniqueId = () => type SortableItem = {
`${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; dragger: Draggable;
element: HTMLDivElement;
index: number;
setIndex: (idx: number) => void;
};
const Panel: React.FC<PanelProps> = ({ const Panel: React.FC<PanelProps> = ({
selectedZone, selectedZone,
setSelectedZone, setSelectedZone,
hiddenPanels, hiddenPanels,
setZonesData,
waitingPanels,
}) => { }) => {
const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({}); // const rowSize = 160;
const [panelDimensions, setPanelDimensions] = useState<{ // const colSize = 160;
[side in Side]?: { width: number; height: number }; const [rowSize, setRowSize] = useState<number>(160);
}>({}); const [colSize, setColSize] = useState<number>(160);
const [openKebabId, setOpenKebabId] = useState<string | null>(null); const [openKebabId, setOpenKebabId] = useState<string | null>(null);
const { isPlaying } = usePlayButtonStore(); const [initialItems, setInitialItems] = useState<Widget[]>([]);
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 { visualizationSocket } = useSocketStore();
const [canvasDimensions, setCanvasDimensions] = useState({ const [canvasDimensions, setCanvasDimensions] = useState({
width: 0, width: 0,
height: 0, height: 0,
}); });
const [panelDimensions, setPanelDimensions] = useState<{
[side in Side]?: { width: number; height: number };
}>({});
const { projectId } = useParams(); const { projectId } = useParams();
const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({});
const { userId, organization, email } = getUserData(); const { userId, organization, email } = getUserData();
// 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 // Track canvas dimensions
useEffect(() => { useEffect(() => {
const canvas = document.getElementById("real-time-vis-canvas"); const canvas = document.getElementById("real-time-vis-canvas");
if (!canvas) return; if (!canvas) return;
@ -98,8 +152,10 @@ const Panel: React.FC<PanelProps> = ({
Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25), Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25),
170 // Min 170px 170 // Min 170px
); );
useEffect(() => {
setInitialItems(selectedZone.widgets);
}, [selectedZone]);
// Define getPanelStyle
const getPanelStyle = useMemo( const getPanelStyle = useMemo(
() => (side: Side) => { () => (side: Side) => {
const currentIndex = selectedZone.panelOrder.indexOf(side); const currentIndex = selectedZone.panelOrder.indexOf(side);
@ -140,27 +196,139 @@ const Panel: React.FC<PanelProps> = ({
}, },
[selectedZone.panelOrder, panelSize] [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 return {
const handleDrop = (e: React.DragEvent, panel: Side) => { ...prev,
e.preventDefault(); widgets: updatedWidgets,
const { draggedAsset } = useWidgetStore.getState(); };
if ( });
!draggedAsset || setInitialItems(selectedZone.widgets);
isPanelLocked(panel) ||
hiddenPanels[selectedZone.zoneUuid]?.includes(panel)
)
return;
const currentWidgetsCount = getCurrentWidgetCount(panel);
const maxCapacity = calculatePanelCapacity(panel);
if (currentWidgetsCount < maxCapacity) {
addWidgetToPanel(draggedAsset, panel);
}
}; };
// Check if panel is locked useEffect(() => { }, [initialItems]);
let fromIndex = useRef<Number>();
let toIndex = useRef<Number>();
let itemsRef = useRef<any>();
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) => const isPanelLocked = (panel: Side) =>
selectedZone.lockedPanels.includes(panel); selectedZone.lockedPanels.includes(panel);
@ -174,6 +342,7 @@ const Panel: React.FC<PanelProps> = ({
const CHART_HEIGHT = panelSize - 10; const CHART_HEIGHT = panelSize - 10;
const dimensions = panelDimensions[panel]; const dimensions = panelDimensions[panel];
if (!dimensions) { if (!dimensions) {
return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities return panel === "top" || panel === "bottom" ? 5 : 3; // Fallback capacities
} }
@ -197,65 +366,40 @@ const Panel: React.FC<PanelProps> = ({
organization, organization,
zoneUuid: selectedZone.zoneUuid, zoneUuid: selectedZone.zoneUuid,
widget: newWidget, widget: newWidget,
projectId, userId projectId,
userId,
}; };
if (visualizationSocket) { if (visualizationSocket) {
visualizationSocket.emit("v1:viz-widget:add", addWidget); visualizationSocket.emit("v1:viz-widget:add", addWidget);
} }
setSelectedZone((prev) => ({ setSelectedZone((prev: any) => ({
...prev, ...prev,
widgets: [...prev.widgets, newWidget], widgets: [...prev.widgets, newWidget],
})); }));
}; };
// Observe panel dimensions // Handle drop event
useEffect(() => { const handleDrop = (e: React.DragEvent, panel: Side) => {
const observers: ResizeObserver[] = []; e.preventDefault();
const currentPanelRefs = panelRefs.current;
selectedZone.activeSides.forEach((side) => { const { draggedAsset } = useWidgetStore.getState();
const element = currentPanelRefs[side]; if (
if (element) { !draggedAsset ||
const observer = new ResizeObserver((entries) => { isPanelLocked(panel) ||
for (const entry of entries) { hiddenPanels[selectedZone.zoneUuid]?.includes(panel)
const { width, height } = entry.contentRect; )
setPanelDimensions((prev) => ({ return;
...prev,
[side]: { width, height },
}));
}
});
observer.observe(element); const currentWidgetsCount = getCurrentWidgetCount(panel);
observers.push(observer); const maxCapacity = calculatePanelCapacity(panel);
}
});
return () => { if (currentWidgetsCount < maxCapacity) {
observers.forEach((observer) => observer.disconnect()); addWidgetToPanel(draggedAsset, panel);
}; } else {
}, [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,
};
});
}; };
// Calculate capacities and dimensions
const topWidth = getPanelStyle("top").width; const topWidth = getPanelStyle("top").width;
const bottomWidth = getPanelStyle("bottom").height; const bottomWidth = getPanelStyle("bottom").height;
const leftHeight = getPanelStyle("left").height; const leftHeight = getPanelStyle("left").height;
@ -265,7 +409,6 @@ const Panel: React.FC<PanelProps> = ({
const bottomCapacity = calculatePanelCapacity("bottom"); const bottomCapacity = calculatePanelCapacity("bottom");
const leftCapacity = calculatePanelCapacity("left"); const leftCapacity = calculatePanelCapacity("left");
const rightCapacity = calculatePanelCapacity("right"); const rightCapacity = calculatePanelCapacity("right");
return ( return (
<> <>
<style> <style>
@ -275,7 +418,6 @@ const Panel: React.FC<PanelProps> = ({
--bottomWidth: ${bottomWidth} ; --bottomWidth: ${bottomWidth} ;
--leftHeight: ${leftHeight}; --leftHeight: ${leftHeight};
--rightHeight: ${rightHeight}; --rightHeight: ${rightHeight};
--topCapacity: ${topCapacity}; --topCapacity: ${topCapacity};
--bottomCapacity: ${bottomCapacity}; --bottomCapacity: ${bottomCapacity};
--leftCapacity: ${leftCapacity}; --leftCapacity: ${leftCapacity};
@ -286,10 +428,6 @@ const Panel: React.FC<PanelProps> = ({
{selectedZone.activeSides.map((side) => ( {selectedZone.activeSides.map((side) => (
<div <div
key={side}
id={`panel-wrapper-${side}`}
className={`panel ${side}-panel absolute ${hiddenPanels[selectedZone.zoneUuid]?.includes(side) ? "hidePanel" : ""
}`}
style={getPanelStyle(side)} style={getPanelStyle(side)}
onDrop={(e) => handleDrop(e, side)} onDrop={(e) => handleDrop(e, side)}
onDragOver={(e) => e.preventDefault()} onDragOver={(e) => e.preventDefault()}
@ -300,39 +438,53 @@ const Panel: React.FC<PanelProps> = ({
delete panelRefs.current[side]; delete panelRefs.current[side];
} }
}} }}
key={side}
id={`panel-wrapper-${side}`}
className={`panel ${side}-panel absolute ${hiddenPanels[selectedZone.zoneUuid]?.includes(side)
? "hidePanel"
: ""
}`}
> >
<div <div
className={`panel-content ${waitingPanels === side ? `${side}-closing` : "" className="container"
}${!hiddenPanels[selectedZone.zoneUuid]?.includes(side) && // ref={containerRef}
waitingPanels !== side ref={(el) => {
? `${side}-opening` if (el) containerRef.current[side] = el;
: ""
} ${isPlaying ? "fullScreen" : ""}`}
style={{
pointerEvents:
selectedZone.lockedPanels.includes(side) ||
hiddenPanels[selectedZone.zoneUuid]?.includes(side)
? "none"
: "auto",
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
}} }}
style={{ position: "relative", width: "100%" }}
> >
{selectedZone.widgets {initialItems
.filter((w) => w.panel === side) .filter((w) => w.panel === side)
.map((widget, index) => ( .map((label, index) => (
<DraggableWidget <div
hiddenPanels={hiddenPanels} className="list-item chart-container"
widget={widget} key={label.id}
key={widget.id} data-panel={label.panel}
index={index}
onReorder={(fromIndex, toIndex) => ref={(el) => {
handleReorder(fromIndex, toIndex, side) if (!itemRefs.current[side]) itemRefs.current[side] = [];
} if (el) itemRefs.current[side]![index] = el;
openKebabId={openKebabId} }}
setOpenKebabId={setOpenKebabId}
selectedZone={selectedZone} style={{
setSelectedZone={setSelectedZone} position: "absolute",
/> left: 0,
height: ["left", "right"].includes(side) ? rowSize : "",
width: ["left", "right"].includes(side) ? "" : colSize,
cursor: "grab",
userSelect: "none",
// marginBottom: side === "left" || side === "right" ? "10px" : "",
// marginRight: side === "top" || side === "bottom" ? "10px" : "",
}}
>
<DraggableWidget
widget={label}
index={index}
openKebabId={openKebabId}
setOpenKebabId={setOpenKebabId}
/>
</div>
))} ))}
</div> </div>
</div> </div>

View File

@ -178,7 +178,6 @@
color: var(--accent-color); color: var(--accent-color);
} }
} }
} }
} }
@ -199,8 +198,10 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 10px; padding: 10px;
display: flex; // display: flex;
flex-direction: column; // flex-direction: column;
display: block;
position: relative;
gap: 6px; gap: 6px;
background: var(--background-color); background: var(--background-color);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
@ -217,9 +218,18 @@
box-shadow: var(--box-shadow-medium); box-shadow: var(--box-shadow-medium);
background: var(--background-color); background: var(--background-color);
position: relative; position: absolute;
padding: 0 10px; padding: 0 10px;
animation: scaleFadeIn 0.4s forwards; // position: absolute;
top: 0;
left: 0;
user-select: none;
z-index: 1;
// animation: scaleFadeIn 0.4s forwards;
.chart-container.dragging {
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
transition: box-shadow 0.2s ease;
}
.kebab { .kebab {
width: 30px; width: 30px;
@ -370,9 +380,6 @@
} }
} }
// Side Buttons // Side Buttons
.side-button-container { .side-button-container {
position: absolute; position: absolute;
@ -430,7 +437,7 @@
path { path {
stroke: var(--text-button-color); stroke: var(--text-button-color);
strokeWidth: 2; stroke-width: 2;
} }
} }
@ -442,7 +449,7 @@
path { path {
stroke: #f65648; stroke: #f65648;
strokeWidth: 1.3; stroke-width: 1.3;
} }
} }
} }
@ -981,3 +988,164 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
// Drad
.container {
position: absolute;
// top: 50%;
// left: 50%;
top: 0;
left: 0;
display: flex;
flex-direction: column;
gap: 12px;
min-width: 160px;
height: 100%;
opacity: 0;
visibility: hidden;
// transform: translate(-50%, -50%);
pointer-events: all;
}
// .list-item {
// position: absolute;
// top: 0;
// left: 0;
// height: 90px;
// width: 100%;
// }
.panel {
background: var(--background-color);
backdrop-filter: blur(10px);
pointer-events: all;
padding: 6px;
&.left-panel,
&.right-panel {
min-width: 150px;
.panel-content {
flex-direction: column;
width: 100%;
gap: 6px;
.chart-container {
position: relative;
width: 100%;
min-height: 150px;
max-height: 100%;
border-radius: #{$border-radius-medium};
box-shadow: var(--box-shadow-medium);
padding: 6px 0;
background: var(--background-color);
}
}
}
&.top-panel,
&.bottom-panel {
left: 0;
right: 0;
min-height: 150px;
.container {
display: flex;
flex-direction: row;
.chart-container {
height: 100%;
}
}
.panel-content {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
min-height: 150px;
}
}
}
.chart-container {
width: 100%;
min-height: 150px;
max-height: 100%;
border-radius: #{$border-radius-medium};
box-shadow: var(--box-shadow-medium);
background: var(--background-color);
position: relative;
// margin: 6px 0;
.item-content {
height: 100%;
padding: 6px;
border-radius: 6px;
padding: 6px 0;
.kebab {
width: 30px;
height: 30px;
position: absolute;
top: 0px;
right: 0px;
z-index: 10;
cursor: pointer;
@include flex-center;
}
.kebab-options {
position: absolute;
top: 18px;
right: 5px;
transform: translate(0px, 0);
background: var(--background-color);
z-index: 10;
display: flex;
flex-direction: column;
gap: 6px;
border-radius: #{$border-radius-small};
box-shadow: var(--box-shadow-medium);
.btn {
display: flex;
gap: 6px;
align-items: center;
padding: 5px 10px;
color: var(--text-color);
&:hover {
background: var(--highlight-accent-color);
width: 100%;
.label {
color: var(--accent-color);
}
svg {
&:first-child {
fill: var(--accent-color);
}
&:last-child {
fill: auto;
stroke: var(--accent-color);
}
}
}
}
.btn-blur {
color: var(--text-disabled);
cursor: not-allowed;
pointer-events: none;
}
}
canvas {
height: 80%;
}
.order {
display: none;
}
}
}