346 lines
10 KiB
TypeScript
346 lines
10 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import { arrayMove } from "@dnd-kit/sortable";
|
|
import { useAsset3dWidget, useSocketStore } from "../../../../store/builder/store";
|
|
import { usePlayButtonStore } from "../../../../store/usePlayButtonStore";
|
|
import { useWidgetStore } from "../../../../store/useWidgetStore";
|
|
import { DraggableWidget } from "../2d/DraggableWidget";
|
|
|
|
type Side = "top" | "bottom" | "left" | "right";
|
|
|
|
interface Widget {
|
|
id: string;
|
|
type: string;
|
|
title: string;
|
|
panel: Side;
|
|
data: any;
|
|
}
|
|
|
|
interface PanelProps {
|
|
selectedZone: {
|
|
zoneName: string;
|
|
activeSides: Side[];
|
|
panelOrder: Side[];
|
|
lockedPanels: Side[];
|
|
points: [];
|
|
zoneId: string;
|
|
zoneViewPortTarget: number[];
|
|
zoneViewPortPosition: number[];
|
|
widgets: Widget[];
|
|
};
|
|
setSelectedZone: React.Dispatch<
|
|
React.SetStateAction<{
|
|
zoneName: string;
|
|
activeSides: Side[];
|
|
panelOrder: Side[];
|
|
lockedPanels: Side[];
|
|
points: [];
|
|
zoneId: string;
|
|
zoneViewPortTarget: number[];
|
|
zoneViewPortPosition: number[];
|
|
widgets: Widget[];
|
|
}>
|
|
>;
|
|
hiddenPanels: any;
|
|
setZonesData: React.Dispatch<React.SetStateAction<any>>;
|
|
waitingPanels: any;
|
|
}
|
|
|
|
const generateUniqueId = () =>
|
|
`${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
const Panel: React.FC<PanelProps> = ({
|
|
selectedZone,
|
|
setSelectedZone,
|
|
hiddenPanels,
|
|
setZonesData,
|
|
waitingPanels,
|
|
}) => {
|
|
const { widgetSelect, setWidgetSelect } = useAsset3dWidget();
|
|
const panelRefs = useRef<{ [side in Side]?: HTMLDivElement }>({});
|
|
const [panelDimensions, setPanelDimensions] = useState<{
|
|
[side in Side]?: { width: number; height: number };
|
|
}>({});
|
|
const [openKebabId, setOpenKebabId] = useState<string | null>(null);
|
|
const { isPlaying } = usePlayButtonStore();
|
|
const { visualizationSocket } = useSocketStore();
|
|
const [canvasDimensions, setCanvasDimensions] = useState({
|
|
width: 0,
|
|
height: 0,
|
|
});
|
|
|
|
// Track canvas dimensions
|
|
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);
|
|
}, []);
|
|
|
|
// Calculate panel size
|
|
const panelSize = Math.max(
|
|
Math.min(canvasDimensions.width * 0.25, canvasDimensions.height * 0.25),
|
|
170 // Min 170px
|
|
);
|
|
|
|
// Define getPanelStyle
|
|
const getPanelStyle = useMemo(
|
|
() => (side: Side) => {
|
|
const currentIndex = selectedZone.panelOrder.indexOf(side);
|
|
const previousPanels = selectedZone.panelOrder.slice(0, currentIndex);
|
|
const leftActive = previousPanels.includes("left");
|
|
const rightActive = previousPanels.includes("right");
|
|
const topActive = previousPanels.includes("top");
|
|
const bottomActive = previousPanels.includes("bottom");
|
|
|
|
switch (side) {
|
|
case "top":
|
|
case "bottom":
|
|
return {
|
|
minWidth: "170px",
|
|
width: `calc(100% - ${
|
|
(leftActive ? panelSize : 0) + (rightActive ? panelSize : 0)
|
|
}px)`,
|
|
minHeight: "170px",
|
|
height: `${panelSize}px`,
|
|
left: leftActive ? `${panelSize}px` : "0",
|
|
right: rightActive ? `${panelSize}px` : "0",
|
|
[side]: "0",
|
|
};
|
|
case "left":
|
|
case "right":
|
|
return {
|
|
minWidth: "170px",
|
|
width: `${panelSize}px`,
|
|
minHeight: "170px",
|
|
height: `calc(100% - ${
|
|
(topActive ? panelSize : 0) + (bottomActive ? panelSize : 0)
|
|
}px)`,
|
|
top: topActive ? `${panelSize}px` : "0",
|
|
bottom: bottomActive ? `${panelSize}px` : "0",
|
|
[side]: "0",
|
|
};
|
|
default:
|
|
return {};
|
|
}
|
|
},
|
|
[selectedZone.panelOrder, panelSize]
|
|
);
|
|
|
|
// Handle drop event
|
|
const handleDrop = (e: React.DragEvent, panel: Side) => {
|
|
e.preventDefault();
|
|
const { draggedAsset } = useWidgetStore.getState();
|
|
if (
|
|
!draggedAsset ||
|
|
isPanelLocked(panel) ||
|
|
hiddenPanels[selectedZone.zoneId]?.includes(panel)
|
|
)
|
|
return;
|
|
|
|
const currentWidgetsCount = getCurrentWidgetCount(panel);
|
|
const maxCapacity = calculatePanelCapacity(panel);
|
|
|
|
if (currentWidgetsCount < maxCapacity) {
|
|
addWidgetToPanel(draggedAsset, panel);
|
|
}
|
|
};
|
|
|
|
// Check if panel is locked
|
|
const isPanelLocked = (panel: Side) =>
|
|
selectedZone.lockedPanels.includes(panel);
|
|
|
|
// Get current widget count in a panel
|
|
const getCurrentWidgetCount = (panel: Side) =>
|
|
selectedZone.widgets.filter((w) => w.panel === panel).length;
|
|
|
|
// Calculate panel capacity
|
|
const calculatePanelCapacity = (panel: Side) => {
|
|
const CHART_WIDTH = panelSize - 10;
|
|
const CHART_HEIGHT = panelSize - 10;
|
|
|
|
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));
|
|
};
|
|
|
|
// Add widget to panel
|
|
const addWidgetToPanel = async (asset: any, panel: Side) => {
|
|
const email = localStorage.getItem("email") || "";
|
|
const organization = email?.split("@")[1]?.split(".")[0];
|
|
|
|
const newWidget = {
|
|
...asset,
|
|
id: generateUniqueId(),
|
|
panel,
|
|
};
|
|
|
|
let addWidget = {
|
|
organization: organization,
|
|
zoneId: selectedZone.zoneId,
|
|
widget: newWidget,
|
|
};
|
|
|
|
if (visualizationSocket) {
|
|
visualizationSocket.emit("v2:viz-widget:add", addWidget);
|
|
}
|
|
|
|
setSelectedZone((prev) => ({
|
|
...prev,
|
|
widgets: [...prev.widgets, newWidget],
|
|
}));
|
|
};
|
|
|
|
// Observe panel dimensions
|
|
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]);
|
|
|
|
// 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 bottomWidth = getPanelStyle("bottom").height;
|
|
const leftHeight = getPanelStyle("left").height;
|
|
const rightHeight = getPanelStyle("right").height;
|
|
|
|
const topCapacity = calculatePanelCapacity("top");
|
|
const bottomCapacity = calculatePanelCapacity("bottom");
|
|
const leftCapacity = calculatePanelCapacity("left");
|
|
const rightCapacity = calculatePanelCapacity("right");
|
|
|
|
return (
|
|
<>
|
|
<style>
|
|
{`
|
|
:root {
|
|
--topWidth: ${topWidth};
|
|
--bottomWidth: ${bottomWidth} ;
|
|
--leftHeight: ${leftHeight};
|
|
--rightHeight: ${rightHeight};
|
|
|
|
--topCapacity: ${topCapacity};
|
|
--bottomCapacity: ${bottomCapacity};
|
|
--leftCapacity: ${leftCapacity};
|
|
--rightCapacity: ${rightCapacity};
|
|
}
|
|
`}
|
|
</style>
|
|
|
|
{selectedZone.activeSides.map((side) => (
|
|
<div
|
|
key={side}
|
|
id="panel-wrapper"
|
|
className={`panel ${side}-panel absolute ${
|
|
hiddenPanels[selectedZone.zoneId]?.includes(side) ? "hidePanel" : ""
|
|
}`}
|
|
style={getPanelStyle(side)}
|
|
onDrop={(e) => handleDrop(e, side)}
|
|
onDragOver={(e) => e.preventDefault()}
|
|
ref={(el) => {
|
|
if (el) {
|
|
panelRefs.current[side] = el;
|
|
} else {
|
|
delete panelRefs.current[side];
|
|
}
|
|
}}
|
|
>
|
|
<div
|
|
className={`panel-content
|
|
${waitingPanels === side ? `${side}-closing` : ""}
|
|
${
|
|
!hiddenPanels[selectedZone.zoneId]?.includes(side) && waitingPanels !== side
|
|
? `${side}-opening`
|
|
: ""
|
|
}
|
|
${isPlaying ? "fullScreen" : ""}`}
|
|
style={{
|
|
pointerEvents:
|
|
selectedZone.lockedPanels.includes(side) ||
|
|
hiddenPanels[selectedZone.zoneId]?.includes(side)
|
|
? "none"
|
|
: "auto",
|
|
opacity: selectedZone.lockedPanels.includes(side) ? "0.8" : "1",
|
|
}}
|
|
>
|
|
{selectedZone.widgets
|
|
.filter((w) => w.panel === side)
|
|
.map((widget, index) => (
|
|
<DraggableWidget
|
|
hiddenPanels={hiddenPanels}
|
|
widget={widget}
|
|
key={widget.id}
|
|
index={index}
|
|
onReorder={(fromIndex, toIndex) =>
|
|
handleReorder(fromIndex, toIndex, side)
|
|
}
|
|
openKebabId={openKebabId}
|
|
setOpenKebabId={setOpenKebabId}
|
|
selectedZone={selectedZone}
|
|
setSelectedZone={setSelectedZone}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Panel;
|